Rust async programming with tokio

Posted on Jul 6, 2023 DRAFT

Project page: tokio.rs

Introduction

Provides a multi-threaded runtime for executing async code, as well as an async version of the standard library

It is based on Rust await and async feature.

When not to use Tokio

  • Tokio is designed for IO-bound applications where applications spend most of time waiting for IO. For CPU-bound applications, use Rust rayon.
  • Reading a lot of files – no advantage compared to an ordinary threadpool.
  • Sending a single web request.

What is async programming?

When encountering an operation that cannot be completed immediately, the program blocks until the operation completes. In the case of async programming, operations that cannot complete immediately are suspended in the background. When the operation is done, the task is unsuspended and the program continues.

#[tokio::main]
async fn main() {

}


// Gets transformed into

fn main() {
    let mut rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("hello");
    })
}

tokio::net::TcpListener

Tokio allows async code to work on many tasks concurrently, sometimes on a single thread.

Spawning tasks

tokio::spawn(async move {
	process(socket).await;
});

Tasks are async green threads. spawn returns a JoinHandle with which the caller can interact with the thread. awaiting the JoinHandle gets the thread’s return value.

let handle = tokio::spawn(async {
	42
});
let out = handle.await.unwrap();

The type lifetime of a spawned task must be 'static. Spawned tasks cannot contain references to data owned outside the task.

This doesn’t compile because variables are not moved into async blocks by default.

let v = vec![1, 2, 3];
task::spawn(async {
	println!("Here's a vec: {:?}", v);
});

// Fix
task::spawn(async move { ... });

Data sharing is enabled by synchronization primitives such as Arc.

Spawned task must implement Send so the runtime can move tasks between threads. They are Send when all data held across await calls is Send.

// This works

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // The scope forces `rc` to drop before `.await`.
        {
            let rc = Rc::new("hello");
            println!("{}", rc);
        }

        // `rc` is no longer used. It is **not** persisted when
        // the task yields to the scheduler
        yield_now().await;
    });
}

// This doesn't work

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        let rc = Rc::new("hello");

        // `rc` is used after `.await`. It must be persisted to
        // the task's state.
        yield_now().await;

        println!("{}", rc);
    });
}