How it works
The explanation, with examples you can run.
Lazy, concurrent, cancellable
use std::time::Duration;use tokio::time::{sleep, timeout}; async fn fetch(name: &str, ms: u64) -> String { sleep(Duration::from_millis(ms)).await; // yields to the executor while waiting format!("{name} after {ms} ms")} #[tokio::main]async fn main() { let fut = fetch("lazy", 10); // nothing runs yet: a future is just a value println!("created the future"); println!("{}", fut.await); // now it runs let start = std::time::Instant::now(); let (a, b) = tokio::join!(fetch("a", 100), fetch("b", 100)); // concurrently, one task println!("{a}, {b}, took about {} ms", start.elapsed().as_millis() / 100 * 100); match timeout(Duration::from_millis(20), fetch("slow", 500)).await { Ok(v) => println!("{v}"), Err(_) => println!("gave up after 20 ms; the inner future was dropped"), } let handle = tokio::spawn(fetch("spawned", 5)); // a separate task; must be 'static + Send println!("{}", handle.await.unwrap());}Run on the Rust Playground (Rust 1.99, Tokio 1.53) it printed created the future, lazy after 10 ms, a after 100 ms, b after 100 ms, took about 100 ms, the timeout message, and spawned after 5 ms.
What poll does
Future has one method, poll, which returns Poll::Ready(value) or Poll::Pending. A future that returns Pending has arranged for a waker to be called when it can make progress, for example when a socket becomes readable or a timer fires. The executor then polls it again. Because the generated state machine may hold references into itself, futures are polled through Pin<&mut Self>, which promises they won't be moved. You rarely meet Pin unless you write futures or combinators by hand.
Tasks and the runtime
#[tokio::main]builds a runtime (multi-threaded by default, with one worker per CPU core) and blocks on yourasync fn main.tokio::spawncreates a task, the async equivalent of a green thread. Tasks are cheap; Tokio's tutorial puts one at a single allocation and 64 bytes. On the multi-threaded runtime a task may move between threads at each.await, so its future must beSendand'static. Holding anRcor astd::sync::MutexGuardacross an.awaitmakes it notSend, which is a common compile error.join!runs several futures concurrently inside one task.select!waits for the first of several and drops the rest.
Cancellation and blocking
Dropping a future cancels it: it stops at the last .await it reached and its locals are dropped. timeout and select! work this way. Write async code assuming any .await might be the last one, for example by not leaving shared state half-updated across an .await.
Scheduling is cooperative. A task that runs CPU-heavy code or calls a blocking function (std::thread::sleep, a synchronous file or database client) holds its worker thread and stalls every other task scheduled there. Use the async versions, or move the work to tokio::task::spawn_blocking.
Recent language changes made async easier to write: async fn in traits (Rust 1.75) and async closures (async || { ... }, Rust 1.85).
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.