Concept · Rust

Lazy futures and async runtimes

A Rust future is a state machine that does nothing until polled; a runtime such as Tokio polls it and wakes it when I/O is ready.

3 minasync/awaitTokioFuture traitexecutor

An async fn in Rust returns a future: a value describing work that hasn't started. Nothing runs until the future is awaited or handed to a runtime. That's the opposite of a JavaScript promise, which starts running as soon as it's created.

rust
let fut = fetch_user(42);   // nothing happens yet
let user = fut.await;       // now it runs

How it runs

The compiler turns each async fn into a state machine with one state per .await. A runtime polls it: the future either finishes (Poll::Ready) or registers a waker with the I/O reactor and returns Poll::Pending. When the socket becomes readable, the reactor calls the waker and the executor polls the future again.

The standard library defines the Future trait but ships no runtime. Tokio is the de facto choice for servers: a multi-threaded executor, an epoll/kqueue reactor, timers, and async channels and locks. Thousands of tasks share a few OS threads, which suits workloads that mostly wait on the network — the same idea as the JavaScript event loop, across several threads.

Consequences

  • Cancellation is dropping. select! and timeout cancel a future by not polling it again. No token is needed, unlike Go's context, but work done before the last completed .await stays done, so cancellation safety matters.
  • Never block. A blocking call (std::thread::sleep, synchronous I/O, heavy CPU work) holds a worker thread and stalls every task queued on it. Use async APIs, or spawn_blocking.
  • Spawned futures must be Send + 'static. Anything held across an .await is part of the state machine, so an Rc or a std mutex guard there makes the task impossible to move between threads. See Send and Sync.

The async Rust lesson covers Tokio, join!, spawn, select! and timeouts.

Learn it in depth