Rust’s support for async/await is becoming more and more mature, and in some scenarios it can be significantly more efficient than models such as threads.
Here’s a brief look at how to get started with asynchronous programming in Rust the fastest way possible.
Hello world async/await
In Rust, asynchronous programming is abstracted as a Future trait, similar to a Promise in JavaScript. In recent Rust, Future objects can be created directly using the async keyword.
The async keyword can be used to create a Future of the following type.
- Define a function:
async fn
. - Define a block:
async {}
Future will not execute immediately. To execute the function defined by Future, you need to.
- use
await
- or create a task for that Future in the asynchronous runtime
To create an asynchronous task, you can either
- using
block_on
- using
spawn
Using async/await keywords
Let’s look at some simple introductory examples using tokio as an example to deepen our understanding of these concepts.
|
|
In this example, the hello and world functions both use the async keyword to indicate that the function is to be executed asynchronously. The return values of both functions are originally String, but with the async keyword, the final signatures of the two functions are represented internally as fn hello() -> impl Future<Output=String>
. That is, the return value is a Future type, which, when executed, will return a String type result.
Here we use two methods to execute Future.
In the hello function, world().await
is used to call the world function and wait for the return of the function, which is not of type Future, but of the Future-associated Output type, in this case String.
In addition to using the await keyword directly, we also used tokio::runtime::Runtime::new()
to create the tokio runtime and run our Future in it, namely rt.block_on(hello())
and rt.block_on(async {})
, both .
For async blocks, you can also call await directly.
In fact, tokio provides a very handy annotation (or property) to facilitate the execution of Future tasks in our main function.
Simply add #[tokio::main]
to main, preceded by the async keyword, to execute the await method directly inside it, without having to use the block_on or spawn methods.
Tip: The async
keyword creates a Future, as opposed to .await
which destroys (deconstructs) the Future. So we can also say that the two keywords deconstruct each other, and async { foo.await }
is equivalent to foo
.
Using spawn
While the previous example executed the Future task directly, we can also use spawn to create a task of Future, then have the task execute in parallel and get the result of the task execution.
spawn will start an asynchronous task and return a JoinHandle type result. The task is started, but spawn does not guarantee (wait) for it to finish executing properly. Consider the following code.
If we execute the above code, we will only see the mission started
printed out, but not the output of the asynchronous task. This is because the main function will end before the output of the asynchronous task is executed, the process will exit, and the print statement of the asynchronous task will not have a chance to execute.
At this point, we need to use JoinHandle to ensure that the task is completed.
Here we just need to get the JoinHandle of spawn and use await to wait for the task to finish, thus ensuring that we can exit the main function when all the work is done.
JoinHandle can also be used to get the return value of an asynchronous task, here is an example from the official documentation.
We can also use a mechanism similar to the chan in golang to communicate between different asynchronous tasks.
|
|
The output of the above code is.
As you can see, the main function waits while rx.await until the asynchronous task finishes and sends a message to the chan via tx.send
, then the main function continues with the following steps and exits after printing “mission completed”.
Waiting for multiple asynchronous tasks
There are many times when we may start multiple asynchronous tasks at the beginning and wait for all of them to finish.
tolio provides the tokio::join!
macro for this purpose.
|
|
Note that tokio::join!
returns only when all asynchronous tasks have finished.
You can use the select macro if you want to start several tasks at the same time and only need one to return before continuing with the subsequent processing.
|
|
The result of the execution of the above program is as follows.
First, in the test_select method, we define two other asynchronous tasks that return values of integer and string types, and set different sleep times for each. We call this method 3 times.
- test_select(100, 200, 500).await;
- test_select(200, 100, 500).await;
- test_select(200, 100, 50).await;
The first two parameters are the sleep times of the two asynchronous tasks, and the third parameter is the timeout time. From the parameters used in these three calls, the third timeout is less than the sleep time of the two asynchronous tasks, so the timeout message is printed.
Summary
Here we just got a brief introduction to the tokio-based asynchronous task programming model. tokio actually provides a lot of useful library functions that we can learn more about later.