阶段二 · 异步语言机制

Future:异步的核心抽象

一句话总结

Future 是"一个尚未完成的异步计算"在 Rust 类型系统里的样子.
运行时通过反复调用它的 poll 方法来推进:返回 Pending 表示"还没好,我让出",返回 Ready(值) 表示"完成了,这是结果".
Future 是惰性的--不被 poll 就什么都不做.

承接上一篇的问题

上一篇结尾留下一个问题:Go 把"挂起/唤醒任务"做成了运行时的隐藏行为,而 Rust 没有内建运行时,必须用语言层的抽象来表达"一个可以暂停,之后恢复的任务".这个抽象,就是 Future.

Future 字面意思是"未来"--它代表一个现在还没算出,但未来会有结果的值.
类比点外卖后拿到的订单号:订单号本身不是饭,但它是"饭终将到来"的凭证,可以拿着它去查进度.Future 就是异步结果的"订单号".

Future trait 的定义

剥掉细节,Future 的核心定义只有一个方法:

trait Future {
type Output; // 关联类型:这个 Future 最终会产出什么

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

暂时忽略 PinContext(后面会说),先抓住主干:有一个 poll 方法,它返回 Poll.而 Poll 是个只有两种变体的枚举:

enum Poll<T> {
Ready(T), // 完成了,这是结果
Pending, // 还没好,稍后再 poll 我
}

运行时驱动一个 Future 的方式,就是不断调用 poll.可以把它读成一句问话:"完成了吗?"
Ready(v) 就是"好了,给出结果 v";答 Pending 就是"还没,先忙别的去吧".

轮询模型:谁来 poll,何时 poll

一个自然的担心:难道运行时要像忙轮询那样,对着一个 Pending 的 Future 疯狂重试 poll 吗?不会. 这正是 Rust 异步设计的精妙处.

当 Future 返回 Pending 时,它有一个义务:在让出之前,先把 Context 里携带的 waker(唤醒器)登记到它正在等待的事件源上(比如登记给 epoll).这样:

运行时 ──poll()──▶ Future

┌───────────┴────────────┐
Ready(v) Pending
│ │
任务完成 先存好 waker,再让出

(IO 就绪时)waker 被调用

运行时收到通知 → 再次 poll

所以是"就绪了再唤醒,唤醒了才 poll",不是空转重试.waker 就是连接"事件循环就绪通知"和"重新 poll 这个任务"的那根线.

poll 返回 Pending 时登记 waker,就像打客服电话遇到占线,系统说"留个号码,有空位回拨",而不是让我们一直按重拨.
waker 就是留下的回拨号码.

关键性质:Future 是惰性的

这是 Go 程序员最容易栽跟头的一点.在 Go 里,go f() 一执行,goroutine 立刻开始在后台跑.但在 Rust 里,创建一个 Future 什么都不会发生--它只是个"待办描述",必须有运行时去 poll 它,里面的代码才会执行.

async fn say_hi() {
println!("hi"); // 这行不会仅因创建 Future 而执行
}

fn main() {
let fut = say_hi(); // 创建了 Future,但 "hi" 没有打印!
// fut 从未被 poll,里面的代码一行都没跑
}

一个 Future 若没有被 .await,也没有 spawn 给运行时,它就是一段死代码.编译器通常会给出 unused must_use 警告--看到它别忽略.

记住口诀:Future 不被 poll,等于没写.

Go 的 goroutine 是"点火即发射"(eager):go f() 立刻起跑.
Rust 的 Future 是"蓝图待施工"(lazy):画好图纸(创建 Future),但不递交给施工队(运行时)去 poll,工地上不会动一砖一瓦.
这个差异会贯穿异步 Rust 的全过程.

Future 的本质是状态机

一个能"暂停又恢复"的任务,凭什么记得"上次跑到哪了"?答案是:每个 Future 本质上是一个状态机(state machine).它把"从头到尾的执行过程"切成若干段,段与段之间就是可能让出的暂停点.

设想一个任务:先读文件,再发网络请求,最后返回.它对应的状态机大致是:

状态 0:起始
│ 发起读文件 → 没好? 存 waker,停在状态 1,返回 Pending

状态 1:等文件
│ 文件好了被重新 poll → 发起网络请求 → 没好? 停在状态 2,Pending

状态 2:等网络
│ 网络好了被重新 poll → 拿到结果

状态 3:完成 → 返回 Ready(结果)

每次 poll,状态机从当前状态尽量往前推进,推到一个还没就绪的暂停点就返回 Pending;下次被 poll 时,从上次停下的状态接着跑.当前状态,局部变量等"现场",都保存在这个 Future 结构体里.

几乎不用手写状态机

好消息:这个状态机不需要手动实现.下一篇会看到,async/await 语法让编译器自动把代码翻译成这样的状态机.

本篇理解"Future = 可暂停的状态机,靠 poll 推进,靠 waker 唤醒"即可--这是看懂 async/await 的地基.

关于 Pin 和 Context(先建立印象)

回到 poll 签名里那两个被跳过的参数,这里给个够用的直觉,不展开:

参数 作用直觉
Context 装着 waker 的盒子.Future 要让出时,从这里取出 waker 登记给事件源,以便日后被唤醒.
Pin<&mut Self> 保证这个 Future 在内存里"钉住不动".因为状态机内部可能有指向自身的引用(自引用结构),移动会让指针失效,Pin 防止这种移动.

入门阶段几乎不会直接和 Pin 打交道--它由 async/await 和运行时在幕后处理.知道"它为自引用状态机保驾护航"就足够了.

快速回顾

  • Future:代表一个未完成的异步计算,核心是 poll 方法,产出类型由关联类型 Output 指定.
  • Poll 枚举:Ready(v) 表示完成并给出结果,Pending 表示未就绪,稍后再 poll.
  • 不空转:返回 Pending 前先登记 waker,事件就绪时由 waker 触发"重新 poll",而非忙轮询.
  • 惰性:创建 Future 不执行任何代码;不被 poll(未 await / 未 spawn)等于死代码--与 Go 的 eager goroutine 相反.
  • 状态机:Future 本质是保存执行现场的状态机,在暂停点之间往返推进;下一篇由编译器自动生成.
  • Pin / Context:Context 装 waker;Pin 钉住自引用状态机防移动,入门期由框架代管.

动手练习

  1. 解释 Pending 义务:用自己的话解释 poll 返回 Pending 时,Future 必须先做什么,为什么.
  2. 观察警告:写一段创建了 Future 却不 await 的代码,观察编译器的 unused must_use 警告.
  3. lazy vs eager:把"创建 Future"和 Go 的 go f() 对比,说明 lazy 与 eager 的本质差异.
  4. 画状态机:给一个"先 sleep 1 秒,再返回 42"的任务画出它的状态机(状态,暂停点,转换).
  5. 查阅文档:查阅标准库 std::future::Future 文档,确认 poll 的真实签名与 Poll 的两个变体.