If you are interested in learning about asynchronous programming in more depth, I recommend reading Asynchronous Programming in Rust.

Asynchronous programming

When we run code that makes network requests, these request are sent through the network.

Sending the request and waiting for the response is done by the network peripheral and doesn’t require the CPU. This means, the CPU is free to do other things while it waits.

Code written synchronously will send a request and then block the thread waiting for a response. For example:

1
2
3
4
fn main() {
    let resp = reqwest::blocking::get("https://httpbin.org/ip")?.text()?;
    println!("{:#?}", resp);
}

The code above, makes the request and blocks the thread until a response is ready.

With asynchronous programming, we can take advantage of the thread and use it to do something while we wait for a response.

What’s Tokio

Asynchronous code requires a runtime to execute. More specifically, it requires a scheduler that executes tasks, a timer that executes code after a specified period of time and a driver that takes care of executing asynchronous I/O tasks and responding to events from these tasks. Tokio provides all these things.

Using Tokio

A minimal example of using Tokio runtime looks like this:

1
2
3
4
5
6
fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("Hello world!");
    });
}

The result of running this code is simply printing Hello world! to the console.

The first instruction creates the Tokio runtime:

1
let rt = tokio::runtime::Runtime::new().unwrap();

In the next instruction we use block_on to have the main function wait until the given async function ends.

1
2
3
rt.block_on(async {
    println!("Hello world!");
})

The keyword async creates an async block, which is a block of code that needs to be executed by an asynchronous runtime.

Of course, this is a very complicated way to print a string to the console. We’re just using it to show how the Tokio runtime works.

It is uncommon to create the runtime explicitly like we did above. We will most of the time find code written like this:

1
2
3
4
#[tokio::main]
async fn main() {
    println!("Hello world!");
}

The #[tokio::main] procedural macro takes an async version of main and rewrites it to something similar to our first example.

A more real-world example, where asynchronous programming is actually useful, is executing HTTP requests in parallel. For example:

1
2
3
4
5
6
7
#[tokio::main]
async fn main() {
    let r1 = reqwest::get("https://httpbin.org/ip").await;
    let r2 = reqwest::get("https://google.com").await;
    println!("{}", r1.unwrap().status());
    println!("{}", r2.unwrap().status());
}

In this example, we are calling reqwest to make requests, since it provides an asynchronous API. Calling await on the result of reqwest.get converts it into a task that will be executed by Tokio runtime.

The first request is started and a Future is returned. The second request is immediately started and also returns a Future.

When we call unwrap on r1, the future will be resolved if it’s available. If not, Tokio runtime will continue running other tasks. The main function will be resumed once the r1 future is resolved. Same thing for r2.

Conclusion

Tokio allows us to write asynchronous code without having to worry too much about the implementation.

To take advantage of this, we need to use libraries (in the last example we used reqwest) that provide functions that are compatible with Tokio runtime. This means that sometimes we’ll find libraries that are not compatible with Tokio. Luckly, Tokio is the most popular runtime for Rust, so this shouldn’t happen too often.

As usual, runnable examples can be found in Github.

[ programming  rust  ]
Programming Concurrency in Rust
Smart Pointers in Rust
Rust References Lifetimes
Traits, Rust's Interfaces
Testing in Rust