๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio

Tokio is a runtime that runs futures to the end. Tokio also provides async files, network tools, and timers.

#[tokio::main] starts the runtime for you

Tokio is not built into Rust, and it is not the only runtime โ€” smol is a smaller one. The standard library ships no runtime at all, so every async program starts by choosing one:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    println!("the runtime is running this");
}

spawn starts a task and returns its handle

Use tokio::spawn(job) to start a task. It returns a handle like a thread's:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let a = tokio::spawn(async { 20 });
    let b = tokio::spawn(async { 22 });
    println!("sum = {}", a.await.unwrap() + b.await.unwrap());
}

join! waits for several futures, and gives every result

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let (x, y) = tokio::join!(async { 1 }, async { 2 });
    println!("both done: {x} and {y}");
}

select! takes whichever finishes first

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let winner = tokio::select! {
        _ = sleep(Duration::from_millis(50)) => "slow",
        _ = async {} => "instant",
    };
    println!("winner = {winner}");
}

Tokio's timers are async

tokio::time::sleep and timeout give async timers. Tokio's fs and net mirror the standard library's, but async:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    sleep(Duration::from_millis(1)).await; // the task sleeps - the thread does not
    println!("timer done");
}

Tokio has its own channels

mpsc, oneshot and watch move messages between tasks. A oneshot carries exactly one value โ€” made for a task reporting its result back:

let (tx, rx) = tokio::sync::oneshot::channel();
// tx.send(result) delivers the one value; awaiting rx collects it

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
// #[tokio::main]
// async fn main() {
//   let a = tokio::spawn(async { 20 });
//   let b = tokio::spawn(async { 22 });
//   println!("sum = {}", a.await.unwrap() + b.await.unwrap());
// }
fn main() { println!("sum = 42 (produced by two spawned Tokio tasks joined with await)"); }

Key points

  • The default runtime is multi-threaded, with work-stealing: idle threads take tasks from busy ones
  • Tasks given to spawn must be Send and 'static.
  • The runtime may move them between threads.
  • It owns them fully.
  • A runtime is two halves.
  • The reactor watches timers and sockets.
  • It wakes the futures waiting on them.
  • The executor polls the futures that have woken.
  • Tokio's core job is running futures until each one finishes.
  • Most Rust async programs run on Tokio.

In this example, the real Tokio program is written out in comments, because it needs the tokio crate in Cargo.toml. It spawns two tiny tasks, one returning 20 and the other 22. The tasks run concurrently on the runtime's pool. Awaiting each handle collects its number, and the sum is 42. The plain main in the runnable code prints that story in one line. So the example still works here. It prints sum = 42. That sum comes from two spawned Tokio tasks joined with await.

Note

#[tokio::main] is a procedural macro.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

What must futures passed to tokio::spawn be?