Rust in Everything
基于 async-stream 0.3.6

async-stream 源码解析

全卷只对着一个版本写:

crate版本tagcommit日期
async-stream0.3.6v0.3.6b0b2f222024-10-02
async-stream-impl0.3.6同上同上同上

两个 crate 在同一个仓库、同一个 tag 发布,async-stream 用 =0.3.6 精确依赖 async-stream-impl。

tokio 卷讲的是 future 怎么被 poll、怎么被唤醒;这一卷讲的是怎么写出一个 Stream。手写 Stream 要自己维护状态机,async fn 又只能返回一个值。async-stream 用两个宏补上这一块:在 stream! { ... } 里写普通的 async 代码,想交出一个值就写 yield v;。稳定版 Rust 没有异步生成器,yield 在这里能用,全靠宏在编译期把它改写掉。

全卷只有一条主线:yield 怎么从 async 块里把值递出去。它分成两段:

  • 宏展开(编译期):stream! 是一个 macro_rules!,把 $crate 路径塞给 proc-macro stream_inner;stream_inner 用 syn 的 VisitMut 把每个 yield v 改写成 __yield_tx.send(v).await,把每个 ? 改写成「Err 就发出去然后 return」,把 for await 展开成 next().await 循环,最后把整块包成 AsyncStream::new(rx, async move { ... })。
  • 运行时(每次 poll_next):AsyncStream::poll_next 在自己的栈上放一个 Option<T>,把它的地址写进线程局部变量 STORE,然后 poll 内层 async 块;执行到 send(v).await 时,Send::poll 从 STORE 取出指针、把值写进去,返回 Pending;poll_next 看到槽里有值,返回 Ready(Some(v))。没有通道,没有堆分配。
flowchart TB
  intro[00 导读] --> macro[01 宏展开]
  macro --> runtime[02 运行时]
  runtime --> design[03 设计取舍]
  design --> pit[04 常见坑]
  pit --> site[05 在本站中]
  site --> hands[06 动手]

各部分

  1. 00 导读:锁定的版本、两个 crate 的分工、主线、阅读路线;一个最小例子分别用 stream!、futures::stream::unfold、「通道 + spawn」三种写法写出来,比较它们什么时候执行。
  2. 01 宏展开:stream! 外壳与 stream_inner;Scrub 怎么改写 yield 和 ?,为什么进宏调用、不进闭包;for await 的两步改写。
  3. 02 运行时:yielder.rs 里的 Sender、Receiver、STORE 和 Send future;AsyncStream::poll_next 的每一个分支。
  4. 03 设计取舍:线程局部指针而不是通道;0.3.6 修掉的两个 soundness 洞;和 unfold、通道、原生 gen 块的对比。
  5. 04 常见坑:用报错原文做标题,每个都先跑出最小复现。
  6. 05 在本站中:本站框架自己不用 async-stream,Cargo.lock 里的 0.3.6 来自 sea-orm 和 dioxus-fullstack;哪里可以用、为什么现在不用。
  7. 06 动手:不依赖 async-stream,从零写一个只支持 yield 的最小 stream!。

从「导读」开始。只想会用的读者,读 00 导读和 04 常见坑;想读懂实现的,按顺序读,01 和 02 要连着读:01 的结尾是「改写成 send(v).await」,02 的开头就是「send 返回 Pending」。源码一共七百多行,大半是文档,这一卷比 tokio 生态的其他几卷都短。

💬 关联讨论

发起讨论
加载中...