怎么读这一卷
主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|只想用好它| u["01 全景 → 06 channel → 09 常见坑"]
q -->|为 rayon 卷做准备| r["03 utils → 04 epoch → 05 deque"]
q -->|想读懂内存序| m["02 内存序 → 各章的 Ordering 小节"]
q -->|读懂实现| all["00 到 11 按顺序"]
锁定版本
crossbeam 是一个工作区,六个 crate 各自编号、各自发版。本卷对着同一个发布提交写:
| crate | 版本 | tag |
|---|---|---|
crossbeam(门面) | 0.8.5 | crossbeam-0.8.5 |
crossbeam-channel | 0.5.17 | crossbeam-channel-0.5.17 |
crossbeam-deque | 0.8.8 | crossbeam-deque-0.8.8 |
crossbeam-epoch | 0.9.21 | crossbeam-epoch-0.9.21 |
crossbeam-utils | 0.8.23 | crossbeam-utils-0.8.23 |
crossbeam-queue | 0.3.14 | crossbeam-queue-0.3.14 |
crossbeam-skiplist | 0.1.3 | crossbeam-skiplist-0.1.3(例外,见下) |
前六个 tag 都指向提交 2920c984(2026-09-06,「Prepare for the next release」)。源码链接一律用各 crate 自己的 tag,形如 https://github.com/crossbeam-rs/crossbeam/blob/crossbeam-channel-0.5.17/crossbeam-channel/src/flavors/array.rs#L160。仓库的第一个提交是 2015-05-13,到 2920c984 共 2173 个提交。
发布分支和 main
2920c984 不在 main 上,而在 v0.8 发布分支上。crossbeam 的做法是:修复先合进 main,再挑(cherry-pick)到 v0.8 上发版。写作时(2026-11-01):
2920c984有 174 个提交不在main上。用git log --cherry-mark比对补丁内容,其中 99 个在main上有内容相同的对应提交(cherry-pick 的副本),另外 75 个没有:发版提交、CI 配置同步,以及挑过来时改写过、和main上的版本已经不完全相同的修复;main有 392 个提交不在2920c984上;main上各Cargo.toml的版本号和发布版相同,内容却更新。
所以「读 main 看到的代码」和「你依赖的那个版本」可能不一样。一个会影响用户的例子:main 上的 #1121 让有界 channel 在最后一个接收端 drop 时立即丢弃缓冲里的消息,0.5.17 没有这个修改,消息要等整个通道释放时才析构(实验 E8,见 常见坑:bounded-rx-drop)。全卷以发布版为准;main 上的差异只在相关页末尾用「发布之后」小节提一句,写明 PR 号。
skiplist 为什么用另一个 tag
crossbeam-skiplist-0.1.3(3aa2598e,2024-01-08)在 main 上,不是 2920c984 的祖先。发布分支的工作区把 skiplist 从成员里注释掉了(「Not published from this branch」),那里的 skiplist 目录反而比 0.1.3 更旧:Cargo.toml 写 version = "0.0.0",没有 0.1.2 加的 compare_insert。所以 skiplist 一章以 tag crossbeam-skiplist-0.1.3 为准,行号和链接都用这个 tag。
四条主线
每一页开头都会标出本页在哪条主线上。
主线 1:一条消息。 bounded(1) 上第二次 send 阻塞,recv 把它叫醒。
| 一步 | 源码 | 本卷 |
|---|---|---|
Sender::send 分派到 array flavor | crossbeam-channel/src/channel.rs | API 与 flavor |
start_send:stamp 判定、CAS tail;满了 Backoff 自旋 | flavors/array.rs | array |
Context::with → senders.register → 再查一次 → park | context.rs、waker.rs | Context 与 Waker |
接收方 read → notify → try_select CAS → unpark | flavors/array.rs、waker.rs | 同上 |
全程时序图:一条消息。
主线 2:一个任务。 rayon 的前置。
| 一步 | 源码 | 本卷 |
|---|---|---|
Worker::push,满了 resize 加倍 | crossbeam-deque/src/deque.rs | Chase-Lev 双端队列 |
旧缓冲 defer_unchecked,≥ 1 KiB 立即 flush | 同上 | 扩容与回收 |
Stealer::steal:读 front、pin、读 back、volatile 读、CAS;Steal::Retry | 同上 | Steal、Retry 与批量窃取 |
全程时序图:一个任务。
主线 3:一块内存的回收。
| 一步 | 源码 | 本卷 |
|---|---|---|
epoch::pin() → Local::pin:写本地纪元、SeqCst fence | crossbeam-epoch/src/default.rs、internal.rs | pin 与 Guard |
Atomic::swap → defer_destroy → 本地 Bag → 封袋进全局队列 | atomic.rs、guard.rs、internal.rs | defer 与回收 |
collect → try_advance → 回收落后两个纪元的袋子 | internal.rs、sync/queue.rs、sync/list.rs | 同上;内部的无锁链表和队列 |
全程时序图:一块内存的回收。
主线 4:内存序横线。 不是一条调用链,而是一个贯穿全卷的问题:每遇到一个 Ordering,问它在和谁配对、防的是哪种重排。分类和汇总表在 本库里的内存序,检查工具在 怎么验证并发代码。
阅读路线
- 只想用好它:01 全景,然后 06 channel(特别是和 std、tokio、flume 的两章对照),最后 09 常见坑。
- 为 rayon 卷做准备:03 utils 的
CachePadded和Backoff,04 epoch,05 deque。rayon-core 直接依赖的就是 deque 和 utils,epoch 经 deque 间接进来。 - 想读懂内存序:02 内存序与验证,再读各章的 Ordering 小节。
- 想读懂实现:按顺序读。04 epoch 和 06 channel 是本卷的两个重点。
摘自 crossbeam 和 std 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。源码注释里形如 TODO(名字)、HACK(名字) 的人名,引用时去掉。
前置知识
| 概念 | 本卷用到它的地方 | 已有的讲解 |
|---|---|---|
原子类型和 Ordering 的五个取值 | 全卷 | tokio 本地队列、bytes 的 clone 与提升 |
Send / Sync、UnsafeCell、MaybeUninit | Worker 为什么 !Sync,槽里的消息为什么是 MaybeUninit<T> | — |
thread::park / unpark | channel 阻塞 | tokio 的 driver 与 park |
| 缓存行和伪共享 | CachePadded 及所有头尾指针 | tokio 的任务内存布局 |
| 无锁数据结构里的 ABA 和 use-after-free | epoch 一整部分 | 为什么需要内存回收 从头讲 |
不需要事先读过 C++ 内存模型的规范,也不需要懂 async:crossbeam 里没有 Future。
实验环境与方法
本机:Apple M2 Max(8 个性能核 + 4 个能效核,sysctl hw.cachelinesize 报 128),macOS,rustc 1.99.1。生产服务器不做实验。
实验程序都在 exp-crossbeam/ 下,每个实验一个 crate,从同一个模板复制:依赖锁定 crossbeam =0.8.5、crossbeam-channel =0.5.17、crossbeam-deque =0.8.8、crossbeam-epoch =0.9.21、crossbeam-utils =0.8.23、crossbeam-queue =0.3.14、crossbeam-skiplist =0.1.3,对照用 flume 0.11.1、parking_lot 0.12.5、rayon 1.12.0、loom 0.7.2、tokio 1.54.1。一律离线编译(cargo --offline)。
规矩:
- 每个实验写成「断言 → 方法 → 结果 → 反向验证」。 反向验证是故意造一个「应该失败」或「应该看不到效果」的输入,确认测量方法真的能分辨。
- 性能类一律
--release,至少重复 3 次取中位数,页面上同时给出每一轮的数字。macOS 不能把线程绑到指定核上,线程可能落在能效核上,数字受调度影响。 - 记录负载。 写作期间本机上有别的实验在断续运行,性能类实验开始前要求 1 分钟负载低于 3,页面上写出开始时的
vm.loadavg。 - 正确性类(计数、求和、析构计数、panic 行为)在任何负载下都跑,至少 3 次,输出一致才写。
- 实验输出里的本机路径一律去掉。
本卷的实验:
| # | 名字 | 在哪一页 |
|---|---|---|
| E1 | 伪共享的距离扫描 | CachePadded |
| E2 | AtomicCell 的 lock-free 类型表与锁竞争 | AtomicCell |
| E3 | 长期 pin 让回收停摆 | defer 与回收 |
| E4 | 窃取不丢不重,Retry 的次数 | Chase-Lev 双端队列 |
| E5 | 用 loom 检查一个小 Chase-Lev 模型 | 05 deque |
| E6 | 各 flavor 与 std、flume、tokio 的吞吐 | 和 std::sync::mpmc 对照 |
| E7 | select! 的公平性 | select |
| E8 | 最后一个接收端 drop 时缓冲消息是否析构 | 常见坑:bounded-rx-drop |
| E9 | Backoff 每一步的耗时 | Backoff |
| E10 | ShardedLock 与 RwLock 扫写比例 | ShardedLock |
| E11 | tick 不补发 | at、tick、never |
| E12 | 作用域线程里子线程 panic | thread::scope 与 std 的 scope |
页面上的标注:「推测」表示从源码或数据推出来、但没有实验或文档直接支撑的说法;性能数字都注明测量条件。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本页;源码地图 |
| 01 全景 | crossbeam 解决什么问题;一条消息、一个任务、一块内存的回收 |
| 02 内存序与验证 | 五类 Ordering 用法;loom、Miri、TSan |
| 03 crossbeam-utils | CachePadded、Backoff、AtomicCell、Parker 与 WaitGroup、ShardedLock、作用域线程 |
| 04 crossbeam-epoch | 本卷重点:EBR 的指针类型、pin、defer、回收和内部数据结构 |
| 05 crossbeam-deque | Chase-Lev、扩容、窃取、Injector;rayon 卷前置 |
| 06 crossbeam-channel | 本卷重点:六种 flavor、Context 与 Waker、select、和 std / tokio / flume 对照 |
| 07 queue 与 skiplist | ArrayQueue、SegQueue;并发跳表 |
| 08 设计取舍 | 各章取舍汇总 |
| 09 常见坑 | 报错原文或现象做标题 |
| 10 在本站中 | 本站经 tantivy、rayon、sqlx、pingora 间接依赖的 crossbeam(desktop 构建还经 tao) |
| 11 动手 | 最小 MPMC channel、最小 EBR |
小结
- 六个 crate 锁定在发布提交
2920c984(v0.8分支,2026-09-06),链接用各 crate 自己的 tag;skiplist 例外,用main上的 tagcrossbeam-skiplist-0.1.3。 main比发布版多 392 个提交,其中有会改变行为的修复(#1121);页面以发布版为准,差异写在「发布之后」小节。- 四条主线:一条消息(channel)、一个任务(deque)、一块内存的回收(epoch)、内存序横线。
- 实验在 M2 Max(8P + 4E)上做,性能类 release、≥ 3 次取中位数、记录负载,每个实验都有反向验证。