Rust in Everything
基于 crossbeam 0.8.5

怎么读这一卷

主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
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.5crossbeam-0.8.5
crossbeam-channel0.5.17crossbeam-channel-0.5.17
crossbeam-deque0.8.8crossbeam-deque-0.8.8
crossbeam-epoch0.9.21crossbeam-epoch-0.9.21
crossbeam-utils0.8.23crossbeam-utils-0.8.23
crossbeam-queue0.3.14crossbeam-queue-0.3.14
crossbeam-skiplist0.1.3crossbeam-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 flavorcrossbeam-channel/src/channel.rsAPI 与 flavor
start_send:stamp 判定、CAS tail;满了 Backoff 自旋flavors/array.rsarray
Context::with → senders.register → 再查一次 → parkcontext.rs、waker.rsContext 与 Waker
接收方 read → notify → try_select CAS → unparkflavors/array.rs、waker.rs同上

全程时序图:一条消息。

主线 2:一个任务。 rayon 的前置。

一步源码本卷
Worker::push,满了 resize 加倍crossbeam-deque/src/deque.rsChase-Lev 双端队列
旧缓冲 defer_unchecked,≥ 1 KiB 立即 flush同上扩容与回收
Stealer::steal:读 front、pin、读 back、volatile 读、CAS;Steal::Retry同上Steal、Retry 与批量窃取

全程时序图:一个任务。

主线 3:一块内存的回收。

一步源码本卷
epoch::pin() → Local::pin:写本地纪元、SeqCst fencecrossbeam-epoch/src/default.rs、internal.rspin 与 Guard
Atomic::swap → defer_destroy → 本地 Bag → 封袋进全局队列atomic.rs、guard.rs、internal.rsdefer 与回收
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、MaybeUninitWorker 为什么 !Sync,槽里的消息为什么是 MaybeUninit<T>—
thread::park / unparkchannel 阻塞tokio 的 driver 与 park
缓存行和伪共享CachePadded 及所有头尾指针tokio 的任务内存布局
无锁数据结构里的 ABA 和 use-after-freeepoch 一整部分为什么需要内存回收 从头讲

不需要事先读过 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
E2AtomicCell 的 lock-free 类型表与锁竞争AtomicCell
E3长期 pin 让回收停摆defer 与回收
E4窃取不丢不重,Retry 的次数Chase-Lev 双端队列
E5用 loom 检查一个小 Chase-Lev 模型05 deque
E6各 flavor 与 std、flume、tokio 的吞吐和 std::sync::mpmc 对照
E7select! 的公平性select
E8最后一个接收端 drop 时缓冲消息是否析构常见坑:bounded-rx-drop
E9Backoff 每一步的耗时Backoff
E10ShardedLock 与 RwLock 扫写比例ShardedLock
E11tick 不补发at、tick、never
E12作用域线程里子线程 panicthread::scope 与 std 的 scope

页面上的标注:「推测」表示从源码或数据推出来、但没有实验或文档直接支撑的说法;性能数字都注明测量条件。

目录

部分内容
00 导读本页;源码地图
01 全景crossbeam 解决什么问题;一条消息、一个任务、一块内存的回收
02 内存序与验证五类 Ordering 用法;loom、Miri、TSan
03 crossbeam-utilsCachePadded、Backoff、AtomicCell、Parker 与 WaitGroup、ShardedLock、作用域线程
04 crossbeam-epoch本卷重点:EBR 的指针类型、pin、defer、回收和内部数据结构
05 crossbeam-dequeChase-Lev、扩容、窃取、Injector;rayon 卷前置
06 crossbeam-channel本卷重点:六种 flavor、Context 与 Waker、select、和 std / tokio / flume 对照
07 queue 与 skiplistArrayQueue、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 上的 tag crossbeam-skiplist-0.1.3。
  • main 比发布版多 392 个提交,其中有会改变行为的修复(#1121);页面以发布版为准,差异写在「发布之后」小节。
  • 四条主线:一条消息(channel)、一个任务(deque)、一块内存的回收(epoch)、内存序横线。
  • 实验在 M2 Max(8P + 4E)上做,性能类 release、≥ 3 次取中位数、记录负载,每个实验都有反向验证。

💬 关联讨论

发起讨论
加载中...