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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
async fn world() -> String {
    "world".to_string()
}

async fn hello() -> String {
  // 使用 .await 关键字调用 world() 函数
    let w = world().await;
    format!("hello {} async from function", w)
}

fn main() {
  // 创建运行时
    let rt = tokio::runtime::Runtime::new().unwrap();
    // 使用 block_on 调用 async 函数
    let msg = rt.block_on(hello());
    println!("{}", msg);

    // 使用 block_on 调用 async block
    let _ = rt.block_on(async {
        println!("hello world async from block");
    });
}

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.

1
2
3
async {
    println!("hello world async");
}.await;

In fact, tokio provides a very handy annotation (or property) to facilitate the execution of Future tasks in our main function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#[tokio::main]
async fn main() {
    let msg = hello().await;
    println!("{}", msg);

    async {
        println!("hello world async from block");
    }
    .await;
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("hard work finished");
    });
    println!("mission started");
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let jh = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("hard work finished");
    });
    println!("mission started");

    let _ = jh.await.unwrap();
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use tokio::task;
use std::io;

#[tokio::main]
async fn main() -> io::Result<()> {
    let join_handle: task::JoinHandle<Result<i32, io::Error>> = tokio::spawn(async {
        Ok(5 + 3)
    });

    let result = join_handle.await??;
    assert_eq!(result, 8);
    Ok(())
}

We can also use a mechanism similar to the chan in golang to communicate between different asynchronous tasks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use tokio::sync::oneshot;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("hard work finished");
        tx.send("ping".to_string()).unwrap();
    });

    println!("mission started");
    let _ = rx.await.unwrap();
    println!("mission completed");
}

The output of the above code is.

1
2
3
mission started
hard work finished
mission completed

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let f1 = async {
        sleep(Duration::from_millis(100)).await;
        1
    };
    let f2 = async {
        sleep(Duration::from_millis(50)).await;
        "hello".to_string()
    };

    let (r1, r2): (i32, String) = tokio::join!(f1, f2);
    println!("r1:{}\nr2:{}", r1, r2);

    // 简单等待前面的异步任务结束
    sleep(Duration::from_millis(1000)).await;
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
use tokio::time::{sleep, Duration};

async fn test_select(t1: u64, t2: u64, timeout: u64) {
    let f1 = async {
        sleep(Duration::from_millis(t1)).await;
        1
    };
    let f2 = async {
        sleep(Duration::from_millis(t2)).await;
        "hello".to_string()
    };

    let timeout = sleep(Duration::from_millis(timeout));

    tokio::select! {
        _ = timeout => {
            println!("got timeout!");
        }
        v = f1 => {
            println!("got r1: {}", v);
        }
        v = f2 => {
            println!("got r2: {}", v);
        }
    }
}

#[tokio::main]
async fn main() {
    // got first task result
    test_select(100, 200, 500).await;
    // got second task result
    test_select(200, 100, 500).await;
    // timeout
    test_select(200, 100, 50).await;
}

The result of the execution of the above program is as follows.

1
2
3
got r1: 1
got r2: hello
got timeout!

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.