异步编程基础:future、任务与运行时 — 第 17 章拆解
异步编程基础:future、任务与运行时
这一章讲什么: Rust 的异步模型——第三版新增的一章。主线是一个小网络爬虫: 并发抓两个网页,谁快用谁。全程借它讲清三件事:future 是什么(惰性、要等 await)、 运行时是什么(语言不带,要用 block_on 启动)、并发怎么发生(只发生在 await 点)。 读完你能写 async 函数、知道为什么 main 不能是 async、知道为什么长任务里要 yield。
1. 顶层全景:先分清三个词
并发(concurrency)vs 并行(parallelism):一个人两个任务来回切换是并发;两个人各干各的是并行;单核 CPU 上只有并发,多核才可能并行——async 通常实现并发,底下是否并行取决于运行时1。
为什么需要 async:网络等 I/O 函数是阻塞的——不调完不走。给每个下载开一个线程也行,但线程的资源开销会累积。async 的答案是:不阻塞调用,而是声明一堆任务,让运行时决定执行顺序2。
线程模型:代码现在就要跑(spawn 立即执行) → 并发由操作系统调度
async 模型:代码声明"将来会就绪"(惰性) → 并发由运行时调度,await 点是切换口
图说:spawn 是"立刻开干",async fn 是"先登记,await 时才动工"。2. Future 与 async/await 语法
future 是"现在没好、将来会好"的值;Rust 里实现 Future trait 的类型就是 future。
async fn 在函数上加 async,体内用 await 等另一个 future;每个 await 都是这个函数可能暂停、之后恢复的点;检查 future 好了没有叫 poll3。
主线例子的核心函数:
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await; // 两次网络等待
Html::parse(&response_text).select_first("title")
.map(|title| title.inner_html())
}两个必须记住的机制:① future 是惰性的——不 await 什么都不发生(像迭代器不 next 就不动;不信会收到编译警告)。② async fn 是语法糖:它返回的是一个实现了 Future<Output = 原返回类型> 的匿名类型,函数体被包进一个 async 块4。await 是后缀关键字(写在表达式后面),所以能链式 .await.text().await。
为什么 main 不能是 async(E0752):async 代码需要运行时——一个管理异步代码执行细节的 crate;main 可以初始化运行时,但它本身不是运行时,总得有个非 async 的起点。多数语言把运行时捆在语言里,Rust 不捆——web 服务器和单片机要的运行时完全不同,生态里(tokio 等)各取所需。手工启动的姿势:trpl::block_on(async { ... })——阻塞当前线程直到这个 future 跑完5。
await 点的本质:编译器把 async 块编译成一个状态机(每个 await 点一个状态变体,保存跨点所需的数据);运行时的 executor 负责驱动这个状态机——这就是"暂停/恢复"的实现方式,也是"await 只能出现在 async 里"的原因6。
3. 并发地等:select、join 与任务
- race 两个 future:
trpl::select(fut1, fut2).await返回Either::Left/Right,谁先完成用谁——爬虫"两个网址谁快用谁"就是这么做的。 - spawn_task ≈ async 版的 thread::spawn,返回的句柄本身是 future,
handle.await等它跑完(对应线程的 join)。 - trpl::join(fut1, fut2):不建任务,把两个匿名 future 并发跑到都完成;它是"公平"的——轮流检查两边。join! 宏等任意多个编译期已知数量的 future。
- async 通道:async 版 mpsc,
rx.recv().await不阻塞而是把控制权交还运行时;接收循环用while let Some(v) = rx.recv().await(Rust 还没有 async 版 for)7。
两个 async 新手必撞的坑(书里都演了一遍):① 单个 async 块里的代码顺序执行——消息要并发收发,得把发送和接收拆进两个 async 块再 join;② 发送端不 drop,接收端的 recv 永不返回 None,程序永不退出——解法是发送块用 async move(把 tx 移进去,块结束即 drop)8。
4. 协程纪律:await 点之间没有并发
Rust 只在 await 点把控制权还给运行时;两个 await 点之间是纯同步代码。 于是在 async 块里跑长计算会饿死其他 future。解法序列:在慢操作之间插 trpl::sleep(1ms).await(勉强) → 换 trpl::yield_now().await(只交还控制、不睡,意图明确且更快)9。这是协作式多任务:每个 future 自己决定何时让路,也有责任不让路太久。但 yield 不是免费的——把计算密集任务切碎可能反而更慢;"先测量再优化"的老话在这里同样适用。
5. 底层三件套:poll、Pin、Stream
本章最后掀开盖子,三个进阶件只需知道"是什么、为什么":
Future trait 的真身:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}poll 问一次"好了吗":Ready(值) 或 Pending(并借由 Context/Waker 约定"就绪后叫我")。运行时就是反复 poll 的循环10。
为什么 poll 要 Pin<&mut Self>:async 块编译出的状态机会自引用(某个状态变体里存着指向同一块数据里另一字段的引用)。带自引用的值一旦移动,内部引用就指向旧地址——悬垂。Pin 是一个"钉住不许动"的指针包装,编译器用它保证自引用类型不被移动;所以想把 future 装进 Vec 一起 join 时,要用 pin! 宏钉住再移容器。书给的使用建议很直白:Pin/Unpin 主要给写底层库和运行时的人,日常代码在报错信息里见到能认出来就行11。
Stream = 随时间就绪的序列:poll_next(...) -> Poll<Option<Item>>——外层 Poll 学 Future(可能还没好),内层 Option 学 Iterator(可能有多个)。标准库当时还没有 Stream,生态用 futures crate 的定义;StreamExt::next() 让它可以 .await 着用12。
6. 作者的判断与证据
- "future 惰性对 Rust 的性能承诺是必要的"——设计论证,与迭代器的惰性类比4。
- "Rust 不捆绑运行时,多种运行时服务不同场景"——生态系统设计主张5。
- "await 点之间是同步的;长任务要主动 yield"——机制事实,配了饥饿实验9。
- "Pin 主要给底层库"——使用建议,降低读者焦虑11。
判断(我们的,不是书里的): 本章最反直觉也最重要的一条是 "await 点之间没有并发"—— 它把 async 从"免费的并行"还原成"显式的让路协议":并发不是 async 白送的, 是你每次写 await 时买一次的。这解释了为什么 async Rust 的典型 bug (忘记 await、忘记 yield、发送端不 drop)全是"协议没遵守",而不是语法错误。 如果错,会错在: 某些运行时的调度器可能自动抢占长任务(协作式不是唯一实现), 但语言层的保证确实只有 await 点——书以 trpl/tokio 的常规语义为准,没覆盖特殊调度器。
7. 边界与局限
- 运行时的实现(executor、reactor、Waker 细节)明确超出本章;指了 async book。
unsafe、手写 Future、async trait(当时还不是稳定特性)都只给了轮廓。- 性能比较(async vs 线程)给了定性讨论,无数值基准。
8. 可带走的
- 并发 ≠ 并行:单核可并发,多核才并行;async 通常实现并发。
- future = 现在没好将来会好的值;async fn 返回一个 Future。
- future 惰性:不 await 不执行;编译器会警告未用的 future。
- main 不能是 async:async 需要运行时;用
block_on启动。 - await 是后缀关键字;await 点 = 暂停/恢复点;状态机由编译器生成。
- select 竞速、join/join! 等全部、spawn_task + handle.await 等单个。
- 单 async 块内顺序执行;收发要拆两块 join;发送端要 move 进去才能干净退出。
- await 点之间无并发;长任务插
yield_now().await。 - poll 返回 Ready/Pending;Pin 钉住自引用状态机;Stream = Poll + Option。
- Pin/Unpin 日常只需"报错时认得"。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 并发 vs 并行 | Chapter 17: Fundamentals of Asynchronous Programming | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:89(搜「Parallelism and Concurrency」) |
| future 定义与惰性 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:171(搜「A future is a value」) · text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:260(搜「lazy」) |
| async fn 的等价展开 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:314(搜「impl Future」) |
| main 不能 async / 运行时 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:362(搜「not allowed to be async」) · text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:368(搜「needs a runtime」) |
| 状态机 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:424(搜「state machine」) |
| select / Either | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:472(搜「trpl::select」) |
| join 与公平调度 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:667(搜「trpl::join」) |
| async 通道与 while let | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:716(搜「trpl::channel」) · text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:767(搜「while let」) |
| async move 解决退出 | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:881(搜「async move」) |
| 饥饿与 yield_now | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:968(搜「Yielding Control」) · text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1097(搜「yield_now」) |
| poll 与 Pin | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1505(搜「fn poll」) · text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1554(搜「self-referential」) |
| Stream trait | 同上 | text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1732(搜「trait Stream」) |
Footnotes
-
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 89 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:89,搜「Parallelism and Concurrency」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 70 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:70,搜「overhead of the system」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 171 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:171,搜「A future is a value」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 260 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:260,搜「lazy」)。 ↩ ↩2 -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 362 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:362,搜「not allowed to beasync」)与第 35 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:35,搜「does not」)。 ↩ ↩2 -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 424 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:424,搜「state machine」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 716 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:716,搜「trpl::channel」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 881 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:881,搜「async move」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 968 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:968,搜「Yielding Control」)与第 1136 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1136,搜「cooperative multitasking」)。 ↩ ↩2 -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 1505 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1505,搜「fn poll」)。 ↩ -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 1557 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1557,搜「reference to itself」)与第 1703 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1703,搜「lower-level libraries」)。 ↩ ↩2 -
出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 1732 段(
text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1732,搜「trait Stream」)。 ↩