Async Rust: Tokio, Futures & Patterns

How async/await works in Rust, the Tokio runtime, spawning tasks, channels, timeouts and cancellation, and runtime tuning.

AdvancedUpdated 2026-10-2944K views

Futures are State Machines

A Rust async fn compiles to a state machine: each .await point is a suspension. Futures do nothing until polled — there is no runtime baked into the language, you pick one: Tokio (most common), async-std or smol.

# Cargo.toml: tokio = { version = "1", features = ["full"] }

#[tokio::main]                    // main 本身也是异步
async fn main() {
    // 并发等待两个 future
    let (a, b) = tokio::join!(fetch("x"), fetch("y"));
    println!("{a} {b}");
}

async fn fetch(key: &str) -> String { /* ... */ }

// 阻塞代码必须 spawn_blocking(如同步 IO、CPU 密集)
let data = tokio::task::spawn_blocking(|| heavy_compute()).await?;

Spawning Tasks

use tokio::task;

// spawn 的任务独立于当前 future
let handle = task::spawn(async {
    // 'static 要求:闭包不能借用外部数据
    // 需要共享数据就 move 或用 Arc
    let mut n = 0;
    loop {
        n += 1;
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
});

// 等待结果:JoinHandle 的 await
let v: i32 = handle.await?;

// JoinSet 管理 一组任务
let mut set = task::JoinSet::new();
for i in 0..10 {
    set.spawn(async move { i * 2 });
}
while let Some(res) = set.join_next().await { /* ... */ }
Tasks need 'static futures: borrow from the surrounding scope and the borrow checker rejects the spawn. Move owned data in (often wrapped in Arc) — this is the most common async beginner error.

Channels

use tokio::sync::mpsc;

// mpsc:多生产者单消费者
let (tx, mut rx) = mpsc::channel::<Order>(100);

// 生产者:send 是异步的,满了会等待
let t = tokio::spawn(async move {
    tx.send(Order::new()).await.unwrap();
});

// 消费者
while let Some(o) = rx.recv().await {
    process(o);
}

// 广播(一对多)与 watch(只保留最新值)
// tokio::sync::broadcast / tokio::sync::watch

// 同线程内同步通信用 oneshot(一次性)
let (tx, rx) = tokio::sync::oneshot::channel();

Timeouts and Cancellation

use tokio::time::{timeout, Duration};

// 超时包装:Err(elapsed) 表示超时
match timeout(Duration::from_secs(3), fetch("x")).await {
    Ok(res)  => println!("{res}"),
    Err(_)   => println!("timeout"),
}

// select!:等待第一个完成,其余被丢弃
// (丢弃 = 未来的 poll 不再发生,是安全的取消点)
tokio::select! {
    msg = rx.recv() => println!("{msg:?}"),
    _    = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("no message in 5s");
    }
}

// select! 循环:每个分支用 = 绑定再 break
loop {
    tokio::select! {
        Some(o) = rx.recv() => handle(o),
        true = shutdown.cancelled() => break,
    }
}

Runtime Tuning

  • Default multi-thread runtime: one worker thread per core, work-stealing queue.
  • Flavor: current_thread for CLI or embedded; multi_thread for servers.
  • Bump worker_threads / blocking_threads for DB-heavy or blocking-call-heavy services.
  • Never block in async context — use spawn_blocking; keep it in mind when integrating sync libraries.

Friends & Partners