一句话总结
Future 自己不会跑,需要**运行时(runtime)**来驱动.
运行时由两半组成:**执行器(executor)**负责调度任务,调用 poll;**反应器(reactor)**对接操作系统的 epoll,在 IO 就绪时通过 waker 唤醒对应任务.
这套东西,就是 Rust 版的"Go runtime",只不过是外置的库.
为什么 Future 需要运行时
前面反复强调:Future 是惰性的,不被 poll 就什么都不做.那么--谁来 poll 它?谁在 IO 就绪时把它重新唤醒?谁来管理成千上万个任务谁先谁后?
这些"驱动 Future 前进"的活儿,没有任何语言内建机制承担(回顾第 01 篇:Rust 故意不内建运行时).于是必须有一个外部组件来做,这就是异步运行时.Tokio 就是其中最主流的一个.
Future 是一份份"待办工单"(描述了要做什么,做到哪了),但工单不会自己执行.
运行时是调度中心 + 施工队:它拿起工单 poll(推进施工),工单卡在等材料(IO)时把它挂起,材料到货(IO 就绪)再喊它回来接着干.
运行时的两半:执行器与反应器
把运行时拆开,核心是分工明确的两个部件:
| 部件 | 职责 | 类比 |
|---|---|---|
| 执行器 Executor | 持有任务队列,从中取出就绪任务并调用其 poll,推进状态机 |
调度中心 / CPU 侧 |
| 反应器 Reactor | 对接 epoll/kqueue,登记 IO 关注,IO 就绪时触发对应任务的 waker | IO 监听站 / 第 02 篇的事件循环 |
反应器其实就是第 02 篇讲的"事件循环 + epoll"在运行时里的化身;执行器则是新角色,负责"该 poll 谁,什么时候 poll".waker 是连接这两半的桥梁.
waker:把"就绪"翻译成"重新调度"
第 03 篇说过:Future 返回 Pending 前,要把 Context 里的 waker 登记到它等待的事件源上.现在可以看清这条链的全貌了:
|
所以 waker.wake() 的本质动作是:把这个任务标记为"可以再 poll 了",塞回执行器的就绪队列. 它就是"IO 就绪"这个事件,翻译成"请重新调度该任务"这个动作的转换器.
现在三篇串成了一条线
- 第 02 篇:操作系统层的 epoll 就绪通知 → 对应反应器.
- 第 03/04 篇:Future 状态机 + poll + waker 登记 → 被执行器驱动.
- 本篇:执行器 + 反应器 + waker 三者协作,构成完整运行时.
最小心智模型:block_on
最简单的执行器只做一件事:反复 poll 一个 Future,直到它 Ready.这就是 block_on 的核心,伪代码如下(帮助理解,非真实实现):
|
真实的执行器要复杂得多(要同时管理海量任务,跨线程调度),但内核思想不变:poll → 没好就等 waker → 被唤醒再 poll.第 04 篇的 #[tokio::main],做的就是"创建运行时,然后 block_on async main".
和 Go runtime 的对照
现在可以把 Rust 运行时与 Go runtime 精确对齐了:
| 能力 | Go runtime(内建) | Rust + Tokio(外置库) |
|---|---|---|
| 任务调度 | 内建 G-M-P 调度器 | Tokio 执行器(executor) |
| IO 就绪监听 | 内建 netpoller | Tokio 反应器(reactor,基于 mio) |
| 唤醒机制 | runtime 内部完成 | waker |
| 是否可选/可换 | 不可,焊死在语言里 | 可选,可换 async-std,smol 等 |
Go 把发动机焊死在车里,拧钥匙就走,但换不了.
Rust 把发动机做成可拆卸模块(Tokio 是销量最大的那款),自己选,自己装--多一道安装手续,换来嵌入式到服务器的全场景适配能力.
一个直接的现实后果
因为运行时是外置的,Rust 异步生态里大量库会"绑定某个运行时".比如很多网络库基于 Tokio 编写,项目就得用 Tokio.选运行时是项目早期的一个真实决策.
好在 Tokio 已是事实标准,绝大多数场景跟着它走即可.本系列后续全部基于 Tokio.
单线程还是多线程?
执行器可以只用一个线程跑事件循环(适合任务少,或想避免跨线程开销),也可以用一个线程池,把任务分散到多个线程上,配合工作窃取(work-stealing)调度.后者能同时利用多核,是服务器场景的默认选择.
这正呼应第 01 篇的结论:异步与多线程是叠加的--多线程执行器 + 海量异步任务.Tokio 默认就是多线程执行器.具体怎么配置,怎么 spawn 任务,下一篇正式上手.
快速回顾
- 为何需要运行时:Future 惰性,需有人 poll,唤醒,调度;Rust 不内建,故用外置运行时(Tokio).
- 两半结构:执行器(调度 + poll 任务)+ 反应器(对接 epoll,IO 就绪触发 waker).
- waker:把"IO 就绪"翻译成"将任务塞回就绪队列,重新 poll",连接执行器与反应器.
- block_on 心智:poll → Pending 就等 waker → 被唤醒再 poll,直到 Ready.
- 对照 Go:执行器 ≈ 调度器,反应器 ≈ netpoller,waker ≈ 内部唤醒;区别是 Rust 运行时可选可换.
- 单/多线程:执行器可单线程或多线程池;服务器默认多线程,与异步叠加利用多核.
动手练习
- 描述职责:用自己的话分别描述执行器和反应器的职责,并指出 waker 在两者间的作用.
- 对应部件:把第 02 篇的"事件循环"对应到本篇的哪个部件,说明原因.
- 理解 block_on:照着
block_on伪代码,解释"为什么它在 Pending 时不会忙轮询". - Go 对照:列出 Go runtime 的三个组件,与 Tokio 的对应部件一一配对.
- 预习:查阅 Tokio 文档,找出"多线程运行时"和"当前线程运行时"的构造方式(为下一篇预习).