我第一次在 Rust 里写自定义 Future 时,脑子里最大的问号是:poll 返回 Pending 之后,到底是谁决定什么时候再 poll 它一次?如果是我自己写一个 while 循环轮询,那和同步代码有什么区别?后来把 Waker 翻了个底朝天,才意识到异步编程这套体系,本质上靠的是一个极简的“信号通知 + 重新调度”闭环。你可以把 Future 比作肌肉,把执行器比作中枢,把 Waker 比作神经纤维——没有 Waker 这条神经系统,Rust 异步模型就只是一具华丽的空壳。
这篇文章不打算只讲 API 怎么用。我会从“为什么需要唤醒机制”讲起,深入 RawWaker 和 vtable 的底层形态,再手写一个定时器 Future 和一个最小 block_on,最后聊真实运行时里 Waker 的调度路径和调试经验。无论你是在学 Rust 异步,还是正在给别人封装备忘录,这篇文章都能帮你把 Waker 当成自己手里的工具,而不是玄学。
1. 问题源头:Future 天生“被动”,是谁在背后叫醒它
1.1 一次 poll 决定一生的返回结果
先看标准库里的 Future trait:
rust复制pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
poll 的结果只有两种:Ready(v) 表示完成;Pending 表示还没完成。关键就在这里:Future 本身不是一个“主动执行”的实体,它是一台只能被询问的机器。你问它“好了吗”,它只能回答“好了”或者“没好”。如果它回答“没好”,那谁来问第二次?
同步世界里这个问题很简单——我循环来问就行了。但异步世界里,我们追求的恰恰就是“不要一直问”。一个 I/O Future 可能在等待一条 TCP 连接上迟迟不来的数据,可能等一个 30 秒的定时器,也可能等另一个任务往 channel 里丢数据。如果调用方每隔几毫秒就 poll 一次,CPU 就被白白烧掉了,这种模型不仅没有省资源,反而比阻塞线程更浪费。
所以 Rust 异步设计者做了一个关键决定:poll 返回 Pending 时,必须通过 Context 里的 Waker 告诉调用者“以后有进展了我会喊你”。你不需要循环问,你只需要给我一个“叫号器”。
1.2 轮询循环为什么不现实
想象你去一家生意火爆的餐厅等位。没有叫号系统的时候,你只能每隔几分钟跑到前台问:“轮到我了吗?”如果这家餐厅上菜慢,一个多小时你就要问几十次。如果同时有几百桌客人,大家都这么问,前台就什么也别干了。
有了叫号器之后,你坐在椅子上刷手机,震动一响再起身过去。这叫“事件驱动”,也叫 push 模型。Rust Future 的 poll 本质上还是拉模型,但 Waker 把“何时再拉”的决定权从调用方手里夺回来,交给了真正的等待源。内核说“这个 socket 可读了”,epoll 通知到你的异步运行时,运行时拿出对应的 Waker 喊一声,Future 才被重新 poll。
操作系统层面的事件通知一直是 push 模式,Rust 要把这套语义接入用户态,就需要在 Future 和事件源之间修一条“反向通道”。Waker 就是这条通道。
1.3 神经系统三要素:产生信号、传递信号、响应信号
从这张图看 Rust 异步程序的运转,非常像人体反射弧:
- 感受器 / 末梢:实际等待的事件源,比如 socket 数据到达、定时器到期、channel 有消息。它负责“感知外界变化”。
- 传入神经:Waker。事件源知道“有进展了”,但它不需要知道 Future 怎么执行,它只需要把唤醒信号发出去。
- 中枢:执行器(Executor)。收到 Waker 信号后,把它翻译成“某任务需要重新 poll”,并在合适的时机、合适的线程上驱动 future。
这套神经系统里,Waker 是那个最容易被忽略、却决定了性能 bottom line 的部分。它承担了“两个执行线程之间最小的握手协议”:一方说“你可以继续了”,另一方收到后恢复执行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把 Waker 拆开:数据指针加 vtable,本质是一个胖指针
2.1 RawWaker 和 Waker 的层级关系
在标准库里,Waker 并不是一个复杂对象。它内部持有的是一个 RawWaker:
rust复制pub struct RawWaker {
data: *const (), // 指向任意运行时自定义数据
vtable: &'static RawWakerVTable,
}
pub struct RawWakerVTable {
clone: unsafe fn(*const ()) -> RawWaker,
wake: unsafe fn(*const ()),
wake_by_ref: unsafe fn(*const ()),
drop: unsafe fn(*const ()),
}
我把 Waker 理解为“安全的皮,unsafe 的瓤”。Waker 对外提供安全的 wake()、wake_by_ref()、clone(),但内部实际操作的是来自 vtable 的函数指针。每个异步运行时都可以自由定义自己的 data 和 vtable,这样 Waker 不需要知道你的任务是 Tokio 的还是自定义执行器的,它只需要知道“如何唤醒”。
这也是为什么 Rust 可以做到“运行时无关”。Waker 是一个协议,不是一种实现。
2.2 vtable 里的四个函数到底在什么时机被调用
| 函数 | 调用时机 | 典型实现 |
|---|---|---|
clone |
调用 waker.clone(),或把 Waker 传入共享状态时 |
对底层数据增加引用计数,重新返回 RawWaker |
wake |
外部事件发生时,通知 future 可以重新 poll | 消费一份引用计数,触发任务入队 |
wake_by_ref |
借用方式触发唤醒,不转移所有权 | 只做触 |
