async-stream 源码解析
全卷只对着一个版本写:
| crate | 版本 | tag | commit | 日期 |
|---|---|---|---|---|
| async-stream | 0.3.6 | v0.3.6 | b0b2f22 | 2024-10-02 |
| async-stream-impl | 0.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-macrostream_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 动手]
各部分
- 00 导读:锁定的版本、两个 crate 的分工、主线、阅读路线;一个最小例子分别用
stream!、futures::stream::unfold、「通道 +spawn」三种写法写出来,比较它们什么时候执行。 - 01 宏展开:
stream!外壳与stream_inner;Scrub怎么改写yield和?,为什么进宏调用、不进闭包;for await的两步改写。 - 02 运行时:
yielder.rs里的Sender、Receiver、STORE和Sendfuture;AsyncStream::poll_next的每一个分支。 - 03 设计取舍:线程局部指针而不是通道;0.3.6 修掉的两个 soundness 洞;和
unfold、通道、原生gen块的对比。 - 04 常见坑:用报错原文做标题,每个都先跑出最小复现。
- 05 在本站中:本站框架自己不用 async-stream,
Cargo.lock里的 0.3.6 来自 sea-orm 和 dioxus-fullstack;哪里可以用、为什么现在不用。 - 06 动手:不依赖 async-stream,从零写一个只支持
yield的最小stream!。
从「导读」开始。只想会用的读者,读 00 导读和 04 常见坑;想读懂实现的,按顺序读,01 和 02 要连着读:01 的结尾是「改写成 send(v).await」,02 的开头就是「send 返回 Pending」。源码一共七百多行,大半是文档,这一卷比 tokio 生态的其他几卷都短。
💬 关联讨论
发起讨论加载中...