Async & Futures

async/await lets one thread juggle many tasks, each yielding at an .await point instead of blocking — a lighter alternative to an OS thread per task, and the foundation of Rust web servers.

Why Async

An OS thread costs memory and a context switch. Async instead runs many tasks on a few threads, suspending each task at an .await instead of blocking the whole thread. An executor polls a future, and a waker re-queues it when it can make progress:

Async runtime: a future is polled by an executor and re-queued by a waker

Figure 1 — a future runs only when polled; if it is not ready it yields and the waker re-queues it.

async / await

Calling an async fn produces a future — a value describing work that has not run yet. Nothing happens until the future is awaited. An async function compiles into a state machine that resumes where it left off:

async fn fetch(name: &str) -> String {
    format!("data for {name}")
}

async fn fetch_two() -> (String, String) {
    let a = fetch("a").await;   // yield here; resume when ready
    let b = fetch("b").await;   // then do the second
    (a, b)
}

Executors & the tokio Runtime

The standard library provides the async syntax, but a runtime is needed to actually drive futures. tokio is the de-facto production runtime: its executor polls many tasks and its reactor wakes them when I/O completes:

[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};

#[tokio::main]                 // the runtime starts here
async fn main() {
    let a = async {           // two tasks, run concurrently
        sleep(Duration::from_millis(100)).await;
        println!("task A done");
    };

    let b = async {
        println!("task B done");
    };

    tokio::join!(a, b);       // drive both to completion
}

Async versus Threads

Async shines when many tasks spend most of their time waiting (network, disk, timers); threads shine for CPU-bound work. Rust lets you mix both deliberately:

Async (cooperative)Threads (preemptive)
Many tasks on few threadsOne task per OS thread
Yields only at .awaitSwitched by the OS scheduler
Great for I/O-bound workGreat for CPU-bound work
Tiny per-task overheadLarger per-thread stack/memory
Practice move: run the threads demo in the lab page (#19) and time the sequential versus concurrent paths — then picture each OS thread replaced by a lightweight async task on one core.