Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15. Async/Await Essentials 🔴
15. Async/Await 核心要点 🔴

What you’ll learn:
本章将学到什么:

  • How Rust’s Future trait differs from Go’s goroutines and Python’s asyncio
    Rust 的 Future trait 和 Go goroutine、Python asyncio 到底差在哪
  • Tokio quick-start: spawning tasks, join!, and runtime configuration
    Tokio 快速上手:启动任务、使用 join!、配置运行时
  • Common async pitfalls and how to fix them
    常见 async 陷阱以及修法
  • When to offload blocking work with spawn_blocking
    什么时候该用 spawn_blocking 把阻塞工作甩出去

Futures, Runtimes, and async fn
Future、运行时与 async fn

Rust’s async model is fundamentally different from Go’s goroutines or Python’s asyncio. Understanding three concepts is enough to get started:
Rust 的 async 模型和 Go 的 goroutine、Python 的 asyncio根本差异。真正入门只要先吃透三件事:

  1. A Future is a lazy state machine — calling async fn doesn’t execute anything; it returns a Future that must be polled.
    1. Future 是惰性的状态机:调用 async fn 时什么都不会真正执行,它只会返回一个等待被 poll 的 Future
  2. You need a runtime to poll futures — tokio, async-std, or smol. The standard library defines Future but provides no runtime.
    2. 必须有运行时 才能 poll future,比如 tokioasync-stdsmol。标准库只定义了 Future,但压根没带运行时。
  3. async fn is sugar — the compiler transforms it into a state machine that implements Future.
    3. async fn 只是语法糖:编译器会把它展开成一个实现了 Future 的状态机。
#![allow(unused)]
fn main() {
// A Future is just a trait:
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

// async fn desugars to:
// fn fetch_data(url: &str) -> impl Future<Output = Result<Vec<u8>, Error>>
async fn fetch_data(url: &str) -> Result<Vec<u8>, reqwest::Error> {
    let response = reqwest::get(url).await?;  // .await yields until ready
    let bytes = response.bytes().await?;
    Ok(bytes.to_vec())
}
}

Tokio Quick Start
Tokio 快速上手

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::time::{sleep, Duration};
use tokio::task;

#[tokio::main]
async fn main() {
    // Spawn concurrent tasks (like lightweight threads):
    let handle_a = task::spawn(async {
        sleep(Duration::from_millis(100)).await;
        "task A done"
    });

    let handle_b = task::spawn(async {
        sleep(Duration::from_millis(50)).await;
        "task B done"
    });

    // .await both — they run concurrently, not sequentially:
    let (a, b) = tokio::join!(handle_a, handle_b);
    println!("{}, {}", a.unwrap(), b.unwrap());
}

Async Common Pitfalls
Async 常见陷阱

PitfallWhy It HappensFix
Blocking in async
在 async 里做阻塞操作
std::thread::sleep or CPU work blocks the executor
std::thread::sleep 或重 CPU 工作会把执行器线程直接卡死
Use tokio::task::spawn_blocking or rayon
tokio::task::spawn_blockingrayon
Send bound errors
Send 约束报错
Future held across .await contains !Send type (e.g., Rc, MutexGuard)
.await 保存了 !Send 类型,例如 RcMutexGuard
Restructure to drop non-Send values before .await
重构代码,让这些非 Send 值在 .await 之前就被释放
Future not polled
Future 根本没被 poll
Calling async fn without .await or spawning — nothing happens
只调用 async fn 却没 .await,也没 spawn,结果就是什么都不会发生
Always .await or tokio::spawn the returned future
要么 .await,要么 tokio::spawn
Holding MutexGuard across .await
MutexGuard.await 持有
std::sync::MutexGuard is !Send; async tasks may resume on different thread
std::sync::MutexGuard!Send,而 async 任务恢复时可能换线程
Use tokio::sync::Mutex or drop the guard before .await
改用 tokio::sync::Mutex,或者在 .await 前先释放 guard
Accidental sequential execution
不小心写成串行执行
let a = foo().await; let b = bar().await; runs sequentially
let a = foo().await; let b = bar().await; 天然就是顺序执行
Use tokio::join! or tokio::spawn for concurrency
想并发就用 tokio::join!tokio::spawn
#![allow(unused)]
fn main() {
// ❌ Blocking the async executor:
async fn bad() {
    std::thread::sleep(std::time::Duration::from_secs(5)); // Blocks entire thread!
}

// ✅ Offload blocking work:
async fn good() {
    tokio::task::spawn_blocking(|| {
        std::thread::sleep(std::time::Duration::from_secs(5)); // Runs on blocking pool
    }).await.unwrap();
}
}

Comprehensive async coverage: For Stream, select!, cancellation safety, structured concurrency, and tower middleware, see our dedicated Async Rust Training guide. This section covers just enough to read and write basic async code.
更完整的 async 内容:如果需要继续看 Streamselect!、取消安全、结构化并发和 tower 中间件,请直接去看单独的 Async Rust Training。这一节的目标只是让人能读懂并写出基础 async 代码。

Spawning and Structured Concurrency
任务生成与结构化并发

Tokio’s spawn creates a new asynchronous task — similar to thread::spawn but much lighter:
Tokio 的 spawn 会创建一个新的异步任务,概念上类似 thread::spawn,但成本轻得多:

use tokio::task;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // Spawn three concurrent tasks
    let h1 = task::spawn(async {
        sleep(Duration::from_millis(200)).await;
        "fetched user profile"
    });

    let h2 = task::spawn(async {
        sleep(Duration::from_millis(100)).await;
        "fetched order history"
    });

    let h3 = task::spawn(async {
        sleep(Duration::from_millis(150)).await;
        "fetched recommendations"
    });

    // Wait for all three concurrently (not sequentially!)
    let (r1, r2, r3) = tokio::join!(h1, h2, h3);
    println!("{}", r1.unwrap());
    println!("{}", r2.unwrap());
    println!("{}", r3.unwrap());
}

join! vs try_join! vs select!:
join!try_join!select! 的区别:

MacroBehaviorUse when
join!
join!
Waits for ALL futures
等待所有 future 完成
All tasks must complete
所有任务都必须完成时
try_join!
try_join!
Waits for all, short-circuits on first Err
等待全部,但一遇到 Err 就提前返回
Tasks return Result
任务返回值是 Result
select!
select!
Returns when FIRST future completes
哪个 future 先完成就先返回
Timeouts, cancellation
超时、取消等场景
use tokio::time::{timeout, Duration};

async fn fetch_with_timeout() -> Result<String, Box<dyn std::error::Error>> {
    let result = timeout(Duration::from_secs(5), async {
        // Simulate slow network call
        tokio::time::sleep(Duration::from_millis(100)).await;
        Ok::<_, Box<dyn std::error::Error>>("data".to_string())
    }).await??; // First ? unwraps Elapsed, second ? unwraps inner Result

    Ok(result)
}

Send Bounds and Why Futures Must Be Send
Send 约束,以及为什么 future 往往必须是 Send

When you tokio::spawn a future, it may resume on a different OS thread. This means the future must be Send. Common pitfalls:
当用 tokio::spawn 启动一个 future 时,它后续恢复执行的位置可能已经换成另一个操作系统线程了。所以这个 future 通常必须实现 Send。最常见的坑就在这里:

use std::rc::Rc;

async fn not_send() {
    let rc = Rc::new(42); // Rc is !Send
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    println!("{}", rc); // rc is held across .await — future is !Send
}

// Fix 1: Drop before .await
async fn fixed_drop() {
    let data = {
        let rc = Rc::new(42);
        *rc // Copy the value out
    }; // rc dropped here
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    println!("{}", data); // Just an i32, which is Send
}

// Fix 2: Use Arc instead of Rc
async fn fixed_arc() {
    let arc = std::sync::Arc::new(42); // Arc is Send
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    println!("{}", arc); // ✅ Future is Send
}

Comprehensive async coverage: For Stream, select!, cancellation safety, structured concurrency, and tower middleware, see our dedicated Async Rust Training guide. This section covers just enough to read and write basic async code.
更完整的 async 内容Streamselect!、取消安全、结构化并发和 tower 中间件这些主题,还是继续看专门的 Async Rust Training 更合适。本节只负责把基础 async 写法讲明白。

See also: Ch 5 — Channels for synchronous channels. Ch 6 — Concurrency for OS threads vs async tasks.
继续阅读: 第 5 章:Channel 讲同步 channel,第 6 章:并发 会对比操作系统线程和 async 任务。

Key Takeaways — Async
本章要点:Async

  • async fn returns a lazy Future — nothing runs until you .await or spawn it
    async fn 返回的是惰性 Future,只有 .await 或 spawn 之后它才会真正运行。
  • Use tokio::task::spawn_blocking for CPU-heavy or blocking work inside async contexts
    在 async 上下文里遇到重 CPU 或阻塞工作时,用 tokio::task::spawn_blocking 把它甩出去。
  • Don’t hold std::sync::MutexGuard across .await — use tokio::sync::Mutex instead
    不要把 std::sync::MutexGuard.await 持有,异步场景里改用 tokio::sync::Mutex
  • Futures must be Send when spawned — drop !Send types before .await points
    被 spawn 的 future 往往必须是 Send,因此在 .await 之前就要把 !Send 的值释放掉。

Exercise: Concurrent Fetcher with Timeout ★★ (~25 min)
练习:带超时的并发抓取器 ★★(约 25 分钟)

Write an async function fetch_all that spawns three tokio::spawn tasks, each simulating a network call with tokio::time::sleep. Join all three with tokio::try_join! wrapped in tokio::time::timeout(Duration::from_secs(5), ...). Return Result<Vec<String>, ...> or an error if any task fails or the deadline expires.
写一个异步函数 fetch_all,内部启动三个 tokio::spawn 任务,每个任务都用 tokio::time::sleep 模拟一次网络调用。然后用 tokio::try_join! 把它们合并,并且整个过程外面套上一层 tokio::time::timeout(Duration::from_secs(5), ...)。如果任一任务失败,或者总超时到了,就返回错误;否则返回 Result<Vec<String>, ...>

🔑 Solution 🔑 参考答案
use tokio::time::{sleep, timeout, Duration};

async fn fake_fetch(name: &'static str, delay_ms: u64) -> Result<String, String> {
    sleep(Duration::from_millis(delay_ms)).await;
    Ok(format!("{name}: OK"))
}

async fn fetch_all() -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let deadline = Duration::from_secs(5);

    let (a, b, c) = timeout(deadline, async {
        let h1 = tokio::spawn(fake_fetch("svc-a", 100));
        let h2 = tokio::spawn(fake_fetch("svc-b", 200));
        let h3 = tokio::spawn(fake_fetch("svc-c", 150));
        tokio::try_join!(h1, h2, h3)
    })
    .await??;

    Ok(vec![a?, b?, c?])
}

#[tokio::main]
async fn main() {
    let results = fetch_all().await.unwrap();
    for r in &results {
        println!("{r}");
    }
}