Tokio / Chapter 1 of 6

Async runtime and futures

Get started with async Rust and Tokio: understand futures, async/await, ownership, and runtime setup with small code examples.

4 topicsUpdated
01 / 26

Start here: Rust + Tokio#

Tokio schedules async Rust tasks and provides timers, networking, and I/O.

Create a binary crate with dependencies in Cargo.toml and an entry point in src/main.rs. The full feature enables the APIs used in these examples; enable individual features for a smaller build.

Shellterminal
cargo new tokio-guide
cd tokio-guide
cargo run
TOMLCargo.toml
[package]
name = "tokio-guide"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
Rustsrc/main.rs — your first async program
use std::time::Duration;

#[tokio::main]
async fn main() {
    println!("starting");
    tokio::time::sleep(Duration::from_millis(200)).await;
    println!("finished");
}
What happens
#[tokio::main] creates a runtime, enters it, and runs main. The sleep yields control instead of blocking a worker thread.
02 / 26

Rust basics for async code#

Ownership, borrowing, moves, Result, and Option are the main Rust concepts used in async code.

A value normally has one owner. Passing it into async move transfers ownership into that future. Use clone() to create an independent owned value, or Arc::clone() to share one allocation safely between tasks. A borrow (&T or &mut T) temporarily lends access; a spawned task usually must own its data because it may outlive the current function.

RustOwnership and borrowing
fn length(name: &str) -> usize {
    name.len() // borrows; does not consume name
}

fn main() {
    let name = String::from("Ada");
    println!("{}: {}", name, length(&name));
    let moved = name; // moved is now the owner
    println!("{moved}");
    // println!("{name}"); // error: value moved
}
RustResult and Option
fn parse_port(raw: &str) -> Result<u16, std::num::ParseIntError> {
    raw.parse::<u16>()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let port = parse_port("8080")?; // return early on Err
    let maybe_name: Option<&str> = Some("web");
    println!("{} on {}", maybe_name.unwrap_or("app"), port);
    Ok(())
}
A useful rule
A future moved into tokio::spawn needs owned data and normally Send + 'static. The latter means it contains no short-lived borrowed references; it does not mean the task lives forever.
03 / 26

Futures, async, and .await#

An async function returns a Future. A future describes work; the runtime polls it when it can make progress.

async fn returns immediately with a future. Nothing inside its body runs until the future is polled. .await polls it, and if the work is not ready, suspends this task so other tasks can run. Only an async context can use .await. A suspended future keeps its local state and resumes later.

RustTwo sequential awaits vs two concurrent futures
use std::time::Duration;

async fn fetch(label: &str) -> String {
    tokio::time::sleep(Duration::from_millis(100)).await;
    label.to_owned()
}

#[tokio::main]
async fn main() {
    let a = fetch("A").await;
    let b = fetch("B").await; // starts after A finishes

    let (c, d) = tokio::join!(fetch("C"), fetch("D"));
    // C and D make progress together in this task.
    println!("{a} {b} {c} {d}");
}
Concurrency vs parallelism
join! drives multiple futures in one task, so they can overlap while waiting. spawn creates independent tasks that may also run on multiple worker threads. A future that blocks a thread prevents that worker from doing other work.
04 / 26

Runtime and entry points#

The runtime schedules tasks and provides time, I/O, and networking drivers. Most applications start with #[tokio::main].

RustDefault multi-thread runtime
#[tokio::main]
async fn main() {
    println!("running in Tokio");
}
RustSingle-thread runtime
#[tokio::main(flavor = "current_thread")]
async fn main() {
    println!("one scheduler thread");
}
RustManual runtime at a synchronous boundary
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(2)
        .enable_all() // I/O and time drivers
        .build()?;

    runtime.block_on(async {
        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    });
    Ok(())
}
Avoid nested runtimes
Do not build a second runtime and call block_on from inside an async Tokio task. At synchronous boundaries, use a runtime once; inside async code, just .await.