跳转至

异步编程基础: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. 可带走的

  1. 并发 ≠ 并行:单核可并发,多核才并行;async 通常实现并发。
  2. future = 现在没好将来会好的值;async fn 返回一个 Future。
  3. future 惰性:不 await 不执行;编译器会警告未用的 future。
  4. main 不能是 async:async 需要运行时;用 block_on 启动。
  5. await 是后缀关键字;await 点 = 暂停/恢复点;状态机由编译器生成。
  6. select 竞速、join/join! 等全部、spawn_task + handle.await 等单个。
  7. 单 async 块内顺序执行;收发要拆两块 join;发送端要 move 进去才能干净退出。
  8. await 点之间无并发;长任务插 yield_now().await。
  9. poll 返回 Ready/Pending;Pin 钉住自引用状态机;Stream = Poll + Option。
  10. 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」)


  1. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 89 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:89,搜「Parallelism and Concurrency」)。 ↩

  2. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 70 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:70,搜「overhead of the system」)。 ↩

  3. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 171 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:171,搜「A future is a value」)。 ↩

  4. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 260 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:260,搜「lazy」)。 ↩↩

  5. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 362 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:362,搜「not allowed to be async」)与第 35 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:35,搜「does not」)。 ↩↩

  6. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 424 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:424,搜「state machine」)。 ↩

  7. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 716 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:716,搜「trpl::channel」)。 ↩

  8. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 881 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:881,搜「async move」)。 ↩

  9. 出处:「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」)。 ↩↩

  10. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 1505 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1505,搜「fn poll」)。 ↩

  11. 出处:「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」)。 ↩↩

  12. 出处:「Chapter 17: Fundamentals of Asynchronous Programming」第 1732 段(text/24-ch17-chapter-17-fundamentals-of-asynchronous-programm.txt:1732,搜「trait Stream」)。 ↩