本节摘要:async 函数编译成状态机,await 是"此处可让出"的挂起点;少量线程由运行时调度成千上万的任务,等待不再占用线程。本节讲 async/await 的本质、运行时的分工与 Future 契约,并对比第 8 章的线程路线。读完你能写基本的异步代码并理解它为什么便宜。
第 8 章每线程一条 OS 线程:每条栈预留兆级内存、切换进内核。一万并发连接就是一万条线程——账算不平。异步的思路:等待让出线程,谁就绪谁上。任务挂起时栈被压进堆上的状态机,内存开销以百字节计。
async fn fetch_archive(no: u32) -> String { let meta = fetch_meta(no).await; // 挂起点:等待时让出线程 let body = fetch_body(meta).await; format!("{}{}", meta, body) }
两个关键词的判决:
async fn 把函数体编译成一个实现 Future 契约的状态机,不立即执行;.await 等待另一个 Future 完成,期间把当前线程让给其他任务。Rust 的异步与多数语言有一个关键差异:语言只提供机制,调度交给运行时库。std 不带运行时,工程里用 tokio 或 async-std 等执行器:#[tokio::main] 宏把 main 变成运行时入口。
#[tokio::main] async fn main() { let a = fetch_archive(173); let b = fetch_archive(174); let (ra, rb) = tokio::join!(a, b); // 并发执行,不是顺序 println!("{} / {}", ra, rb); }
Future 只有一个方法 poll:运行时调用它问"好了吗",好了交值,没好交出"什么时候再问"的唤醒凭据(Waker)。整个异步生态建立在这一个小契约上——第 5 章 Trait 契约思想的极致体现。

async fn 编译成一台状态机,每个 .await 是一个暂停点——保存现场、交还控制权、就绪后恢复。理解这一点,异步的全部怪象(Pin、不能递归、阻塞危害)都有了解释。
use std::time::Duration; async fn fetch_page(id: u32) -> String { std::thread::sleep(Duration::from_millis(10)); // 演示用:真实代码是异步 IO format!("档案{}内容", id) } async fn audit(ids: &[u32]) -> u32 { let mut hits = 0; for id in ids { let text = fetch_page(*id).await; // 暂停点:让出执行权 if text.contains("违规") { hits += 1; } } hits } fn main() { let ids = [1u32, 2, 3]; let fut = audit(&ids); // 不执行:只生成状态机 let result = futures_executor_block_on(fut); // 阻塞驱动(示意) println!("命中 {}", result); } fn futures_executor_block_on<F: std::future::Future<Output = u32>>(f: F) -> u32 { // 最小执行器示意:真实执行器由 tokio 等运行时提供 let mut f = Box::pin(f); let waker = noop_waker(); let mut cx = std::task::Context::from_waker(&waker); loop { match f.as_mut().poll(&mut cx) { std::task::Poll::Ready(v) => return v, std::task::Poll::Pending => std::thread::sleep(Duration::from_millis(1)), } } } fn noop_waker() -> std::task::Waker { use std::sync::Arc; struct N; impl std::task::Wake for N { fn wake(self: Arc<Self>) {} } std::task::Waker::from(Arc::new(N)) }
末尾的手工执行器刻意裸露了异步的契约:Future 只有被 poll 才前进,waker 是"就绪了叫我"的回执。生产中绝不手写执行器,这段是解剖标本。
在异步任务里做阻塞调用(thread::sleep、同步 IO、重计算),执行器的线程被钉死,同线程其他任务全部陪葬——症状是延迟毛刺,而不是报错。纪律:异步上下文内只做 .await;必须阻塞时用 spawn_blocking 一类设施移出执行线程;CPU 密集任务另起线程或进程。
| 维度 | 线程 | 异步任务 |
|---|---|---|
| 切换成本 | 内核级,微秒级 | 用户态,纳秒级 |
| 数量级 | 数百 | 数十万 |
| 阻塞容忍 | 各自独立 | 一损俱损 |
| 适合 | CPU 密集、低并发 | 海量连接、IO 密集 |
万级以下并发用线程通常更简单;异步的收益在连接数上万、单连接工作轻的 IO 场景才真正兑现。选型先于优化,用错模型的异步比线程更慢更难查。
异步投产前三问:所有阻塞点是否已识别并移出执行线程;任务取消语义是否明确(.await 中途被丢弃时资源如何释放);超时是否外置包裹而非依赖内部 sleep。第二条最常缺席——默认执行器丢弃 future 时不会执行清理魔法,需要 RAII 守卫兜底。把三问写进异步模块的 PR 模板,毛刺类线上问题的大半可以在合并前拦下。