跳转到内容

Task、RawTask 与 Waker

Future 只是一个可被 poll 的状态机。Tokio 必须额外保存任务状态、调度器引用、Join 结果和引用计数,并把类型擦除后的任务交给统一队列,这就是 Task / RawTask 层存在的原因。

先给答案:Waker 只是重新安排 poll 的通知,不是把 Future 直接执行完

Section titled “先给答案:Waker 只是重新安排 poll 的通知,不是把 Future 直接执行完”

Future 在 poll 时如果暂时没有结果,会返回 Pending,并把自己的 Waker 注册到资源或队列中。IO driver、timer 或 channel 状态变化后调用 wake,executor 再把任务放回可运行队列,下一次 poll 才继续推进状态机。

因此丢唤醒通常来自注册顺序、状态检查和并发修改没有形成正确协议;重复唤醒则通常是可接受的额外调度成本。理解 Waker 要同时看 Future 状态、资源状态和 executor 队列,不能把它当成普通回调。

spawn(Future)
-> task::new_task
-> Cell { header, scheduler, core, trailer }
-> Task<S> 调度器持有
-> Notified<S> 已取得调度许可
-> JoinHandle<T> 调用方持有
RawTask = NonNull<Header>
+-> Vtable { poll, schedule, dealloc, try_read_output, ... }

公开 tokio::spawn 位于 tokio/src/task/spawn.rs:174-185。类型擦除入口 RawTask 位于 tokio/src/runtime/task/raw.rs:20-24,其虚表让不同 Future<Output = T> 能进入同一种调度队列;实际 poll 分派位于 raw.rs:271-281。

worker poll task
-> Harness::poll
-> Future::poll(Context { waker })
+-> Ready(output) -> 保存结果,通知 JoinHandle,完成
+-> Pending -> 清除 running;若期间被通知则重新 schedule
resource event
-> RawWaker::wake / wake_by_ref
-> Harness::wake_by_val / wake_by_ref
-> state.transition_to_notified
-> scheduler.schedule(Notified)

wake_by_val 与 wake_by_ref 在 tokio/src/runtime/task/waker.rs:93-114,分别进入 harness.rs:68-108。状态转换先判断任务是否已经 notified/running,避免每次 wake 都重复入队。

任务 poll 与外部 wake 可以并发。Tokio 不依赖“先 poll 后 wake”的时间顺序,而是把 RUNNING、NOTIFIED、COMPLETE、CANCELLED 等状态压入原子状态机:

poll 返回 Pending 前发现 NOTIFIED
-> 不能休眠
-> 重新交还调度器
wake 发现任务已 NOTIFIED
-> 不重复排队
-> 保留已有调度许可

因此,Waker 的正确性核心不是回调本身,而是 wake 与 poll 退出之间的原子握手。

JoinHandle 定义于 tokio/src/runtime/task/join.rs:163-177。丢弃 JoinHandle 只放弃观察结果,不会自动取消任务;abort 在 join.rs:227-229,通过 RawTask::remote_abort 设置取消并安排任务再次执行清理。

替代方案:让调度队列保存泛型 Future 或为每类 Future 建独立队列。

问题:运行时无法用统一容器管理异构任务,调度器泛型会向整个实现扩散。

设计:固定布局的 Header + 类型专属 Vtable 完成类型擦除,同时保留静态生成的 poll/drop 函数。

场景 风险 处理
自定义 Future 返回 Pending 却未存 Waker 任务永久沉睡 Pending 前注册或确认已有唤醒来源
重复 wake 被理解为计数信号 多次 wake 可能合并 状态数据应单独保存,poll 时读取
drop JoinHandle 期待取消 任务会 detach 继续运行 显式 abort 或使用结构化任务容器
poll 内执行很久 Waker 再快也无法抢占 保持单次 poll 有界

无锁任务系统常把“是否需要调度”建模为状态位,而不是消息次数。通知可以合并,真实业务状态必须放在可重复检查的数据结构中。

面试锚点

  • Waker 为什么只保证重新 poll,不保证完成?
  • RawTask 如何在不保存泛型参数的情况下 poll Future?
  • wake 与 poll 并发时如何避免丢失唤醒和重复入队?