🦀 Rust 异步机制

从 Future / Poll / Waker 到状态机的源码级剖析

基于 rust-lang/rust library/core/src/task 深度解析
2026-03-10 | 技术深度解读

📑 目录

  • 整体架构
  • Future / Poll / Context / Waker
  • Pin 与状态机
  • ready! 宏
  • RawWaker / vtable
  • 性能与反模式
  • 最佳实践
  • 总结

🌊 为什么 Rust 需要异步机制

  • 高并发 I/O 下线程成本高
  • 回调地狱难维护
  • Rust 要把暂停/恢复编码进静态类型
  • 语言层定义协议,运行时负责调度

🏗️ 异步分层架构

async fn → Future 状态机 → poll(cx)
     → Poll::Ready / Pending
     → Context / Waker / Executor

📦 核心源码

文件职责
poll.rsPoll 及组合子
ready.rsready! 宏
wake.rsContext / Waker / RawWaker

🧭 语言层与运行时的边界

  • Future 协议
  • Poll
  • Context
  • Pin
  • 任务队列
  • IO 驱动
  • 定时器
  • 再调度

🔮 Future trait 的本质

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

🗳️ Poll 枚举定义

pub enum Poll { Ready(T), Pending }

把“结果是否就绪”与“何时再调度”分离。

🧩 poll.rs

#[must_use = "this `Poll` may be a `Pending` variant, which should be handled"]
pub enum Poll { Ready(T), Pending }

📜 Ready 与 Pending 的语义契约

  • Ready:结果已完成
  • Pending:当前无法推进,但必须安排唤醒
  • Pending 不是失败,更不是“稍后自己会好”

🛠️ Poll::map

match self {
  Poll::Ready(t) => Poll::Ready(f(t)),
  Poll::Pending => Poll::Pending,
}

✅ Poll::is_ready / is_pending

matches!(*self, Poll::Ready(_))
!self.is_ready()

🔗 Poll 与 Result / Option

  • map_ok / map_err
  • Poll 与错误模型正交组合

🔄 From<T> for Poll<T>

Poll::Ready(value)

普通值可直接抬升为就绪态。

⚡ ready! 宏是什么

let v = ready!(child.poll(cx));

Pending 就立刻向上传播,Ready 才继续。

🧩 ready.rs

match $e {
  Poll::Ready(t) => t,
  Poll::Pending => return Poll::Pending,
}

🆚 ready! 与 ?

机制传播对象
?Error / None
ready!Poll::Pending

🧠 Context 的职责

pub struct Context<'a> {
  waker: &'a Waker,
  local_waker: &'a LocalWaker,
}

❓ 为什么 poll 需要 &mut Context

  • 传递当前唤醒环境
  • 限定只在当前 poll 调用栈内有效

📣 Waker 的抽象目标

代表“把当前任务重新入队”的能力句柄。

🧱 RawWaker 结构

pub struct RawWaker {
  data: *const (),
  vtable: &'static RawWakerVTable,
}

🧩 RawWaker::new

pub const fn new(data: *const (), vtable: &'static RawWakerVTable) -> RawWaker

🧰 RawWakerVTable 四个函数

clonewakewake_by_refdrop
复制句柄消费唤醒借用唤醒释放资源

🧩 wake.rs

clone: unsafe fn(*const ()) -> RawWaker
wake: unsafe fn(*const ())
wake_by_ref: unsafe fn(*const ())
drop: unsafe fn(*const ())

☢️ 为什么 RawWaker 是 unsafe

  • data 完全类型擦除
  • vtable 要自己维护生命周期/引用计数
  • 错了就是 UB

🛡️ Wake trait 的意义

  • 比手写 RawWaker 安全
  • 通常与 Arc<Task> 搭配

🔔 wake 与 wake_by_ref 的差异

  • wake:消费句柄
  • wake_by_ref:不消费句柄

🧵 Waker 的线程安全要求

若用于构造 Waker,clone / wake / wake_by_ref / drop 都必须线程安全。

🏠 LocalWaker 的存在意义

  • 单线程执行器可放宽 Send + Sync
  • 允许底层使用 Rc 等对象

😴 noop waker

RawWakerVTable::new(|_| NOOP, |_| {}, |_| {}, |_| {})

📌 Pin 为什么重要

  • async 状态机常有自引用
  • Pin 保证对象不再被移动

🔽 async fn 如何降糖

async fn f() { io1().await; io2().await; }
// ≈ 枚举状态机 + poll 驱动

🔄 状态机轮询流程

Start → WaitingIo1 → WaitingIo2 → Done

⏱️ Poll 驱动时序图

Executor --poll--> Future
Future --register--> IO
Future <--Pending--
IO ------wake-----> Executor
Executor --poll--> Future

🗺️ 数据流

Executor creates Waker → Context → Future::poll
Pending → store Waker → event ready → wake() → requeue

🚫 为什么不能 busy polling

  • 浪费 CPU
  • 破坏公平性
  • 违背事件驱动模型

🧬 组合子为何高效

  • Future trait 极小
  • Poll 小枚举
  • ready! 编译期展开

🏭 协议 + 执行分离

  • core 定义 what
  • executor 定义 how
  • reactor 定义 when

🧠 类型擦除 + vtable

RawWaker = (*const (), &'static RawWakerVTable)

🪄 零成本组合

  • Poll::map 只是 match
  • async/await 最终变回状态机

📐 核心对象关系图

Future::poll → Context → Waker → RawWaker → VTable → Executor

🧭 一次 await 发生了什么

await child → poll child
Ready(v) 继续
Pending 保存现场 → wake → 再次 poll

⚙️ 性能优化:最小协议面

  • 单一 poll 接口
  • 低调用开销
  • 易内联

🔁 性能优化:唤醒去重

  • 避免重复入队
  • 常见做法:状态位 + CAS

📉 性能优化:减少无谓 clone

  • Waker clone 常带原子开销
  • 可用 will_wake 判定是否复用

❌ Pending 但不注册唤醒

if not_ready() { return Poll::Pending; } // ❌

❌ 缓存过期 waker

  • 旧 waker 不一定代表当前任务位置
  • 必要时更新缓存

❌ 在 Ready 后继续 poll

多数 future 完成后再 poll 都是逻辑错误。

❌ 忽视 Pin 约束

  • 随意 move 自引用 future
  • 错误实现 Unpin

✅ 手写 Future 模板

  1. 定义状态枚举
  2. 只推进一步
  3. Pending 前更新 waker
  4. 完成后进入 Done