Rust Tutorial · Smart Pointers and Concurrency · Lesson 26 of 29

Rust Async and Await

What async Rust is for, how futures and runtimes like Tokio work, and when to use async or threads.

Create a free account to save this page, take notes and track your progress. Sign in

Futures and runtimes

async fn returns a future: a value that does nothing until it's polled. A runtime such as Tokio polls futures, running thousands of I/O-bound tasks (network requests, database calls) on a few threads. The standard library defines futures but not a runtime, so you add one as a dependency:

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

async fn fetch_price(item: &str) -> f64 {
    tokio::time::sleep(Duration::from_millis(100)).await;   // stands in for a network call
    if item == "mug" { 8.5 } else { 24.0 }
}

#[tokio::main]
async fn main() {
    // both requests run concurrently: about 100 ms in total, not 200 ms
    let (mug, teapot) = tokio::join!(fetch_price("mug"), fetch_price("teapot"));
    println!("{mug} + {teapot}");
}

When to use async

  • Async: many concurrent I/O operations (web servers with Axum, clients with reqwest, databases with sqlx).
  • Threads (or Rayon): CPU-heavy work such as parsing or image processing.

Futures are lazy: the example below builds futures with the standard library alone and shows that nothing runs until they're awaited by an executor.

Futures are lazy
use std::future::Future;
use std::pin::pin;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};

struct NoopWaker;
impl Wake for NoopWaker { fn wake(self: Arc<Self>) {} }

// A tiny executor for ready futures; real programs use a runtime such as Tokio.
fn block_on<F: Future>(future: F) -> F::Output {
    let waker = Waker::from(Arc::new(NoopWaker));
    let mut cx = Context::from_waker(&waker);
    let mut future = pin!(future);
    loop {
        if let Poll::Ready(value) = future.as_mut().poll(&mut cx) { return value; }
    }
}

async fn price(item: &str) -> f64 {
    println!("  computing price of {item}");
    if item == "mug" { 8.5 } else { 24.0 }
}

async fn total() -> f64 {
    price("mug").await + price("teapot").await
}

fn main() {
    let future = total();
    println!("future created, nothing ran yet");
    println!("total: {}", block_on(future));
}

Output

future created, nothing ran yet
  computing price of mug
  computing price of teapot
total: 32.5

Run this on your computer — see “Get Started” for setup.

Test yourself

What does calling an async fn return?

  1. The result immediately
  2. A future that does nothing until it's awaited or polled
  3. A new thread
  4. A callback
Show answer

Answer: A future that does nothing until it's awaited or polled

Was this page helpful?