怎么读这一卷
主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|只想用好它| u["01 全景 → 04 API → 06 并行迭代器 → 09 常见坑"]
q -->|读懂调度器| s["02 Registry → 03 Job 与 Latch → 05 sleep"]
q -->|读懂迭代器| i["06 并行迭代器 → 07 slice 与排序"]
q -->|全部| all["00 到 11 按顺序"]
锁定版本
rayon 仓库是一个工作区,里面两个发布 crate 各自编号:
| crate | 版本 | 发布日期 | tag |
|---|---|---|---|
rayon | 1.12.0 | 2026-04-13(RELEASES.md) | v1.12.0,提交 c9ced18(2026-04-14,「Merge pull request #1294」) |
rayon-core | 1.13.0 | 2025-08-12,和 rayon 1.11.0 一起发 | rayon-core-v1.13.0,提交 7af20d7 |
rayon 1.12.0 没有发新的 rayon-core。两个 tag 的关系可以直接核实:
git merge-base --is-ancestor rayon-core-v1.13.0 v1.12.0 && echo ancestor # ancestor
git rev-list --count rayon-core-v1.13.0..v1.12.0 # 17
git diff rayon-core-v1.13.0 v1.12.0 -- rayon-core | wc -l # 0
也就是说,rayon-core-v1.13.0 是 v1.12.0 的祖先,中间 17 个提交没有碰 rayon-core/ 目录(它们改的是 rayon 本体:par_array_windows、Range<char> 在代理区边界的修复、CI 和文档用词)。所以全卷的源码链接统一用 tag v1.12.0,形如 https://github.com/rayon-rs/rayon/blob/v1.12.0/rayon-core/src/registry.rs#L873-L904;链接到 rayon-core 的部分,换成 rayon-core-v1.13.0 内容完全一样。frontmatter 里 rayon-core 的页面写 rayon-core 1.13.0,rayon 的页面写 rayon 1.12.0。
仓库的第一个提交是 2014-10-02(「Initial commit, using normal closures」),到 v1.12.0 共 2295 个提交;rayon-core 是 2017-02-28 从 rayon 里拆出来的(6b70f1b,「extract the "core APIs" into a rayon-core crate」)。工作区的 MSRV 是 1.80,edition 2021。
crossbeam 卷 里有几处 rayon 链接用的是 rayon-core-v1.13.0,另几处用 v1.12.0,按上面的核实结果两者指向同样的内容。
四条主线
每一页开头都会标出本页在哪条主线上。
主线 1:一次 join。 rayon-core 的心脏,迭代器的每一次切分都是它。
| 一步 | 源码 | 本卷 |
|---|---|---|
join → join_context → registry::in_worker:当前线程是不是 worker | join/mod.rs、registry.rs | in_worker:从哪进池 |
栈上建 StackJob{latch: SpinLatch, func, result},push 进本地 deque | job.rs、registry.rs | JobRef 与几种 Job |
跑 A;回头 take_local_job,弹到 B 就 run_inline | join/mod.rs | join 与 join_context |
B 被偷了:wait_until → wait_until_cold,边等边找活 | registry.rs | WorkerThread 主循环 |
窃取者执行 B 后 Latch::set,必要时唤醒原线程 | latch.rs | Latch 家族 |
全程时序图:一次 join。
主线 2:一次 par_iter().sum()。 rayon 本体的骨架。
| 一步 | 源码 | 本卷 |
|---|---|---|
(0..n).into_par_iter().sum() → drive_unindexed(SumConsumer) | range.rs、iter/sum.rs | 两个 trait |
bridge → with_producer → bridge_producer_consumer | iter/plumbing/mod.rs | plumbing |
LengthSplitter::try_split:切就 join_context,不切就 fold_with | 同上 | bridge 与自适应切分 |
回溯时 reducer.reduce(left, right) | iter/sum.rs | fold、reduce 与 sum |
全程时序图:一次 par_iter。
主线 3:一个线程睡下又醒来。 sleep 模块,本卷核心。
| 一步 | 源码 | 本卷 |
|---|---|---|
start_looking:inactive 计数 +1 | sleep/mod.rs、sleep/counters.rs | 05 sleep 模块 |
no_work_found:32 轮 yield_now,announce_sleepy,再找一轮 | sleep/mod.rs | 同上 |
sleep:latch 进 SLEEPY / SLEEPING,查 JEC,登记 sleeping,最后看一眼注入队列,Condvar::wait | sleep/mod.rs、latch.rs | 同上 |
发布方 new_jobs:翻 JEC,按计数叫醒几个 | sleep/mod.rs | 同上 |
全程状态图:一个线程睡下又醒来。
主线 4:内存序与「设置之后不能再碰」横线。 不是一条调用链,而是两个贯穿全卷的问题。一是每遇到一个 Ordering,问它和谁配对,分类沿用 crossbeam 卷的内存序一章。二是 rayon 特有的:Latch::set 之后,等待方可能立刻返回、销毁 latch 所在的栈帧,所以 set 的参数是 *const Self 而不是 &self,SpinLatch::set 要先把需要的字段读出来再置位。这一条在 Latch 家族 集中讲,其余各页遇到就点一下。
阅读路线
- 只想用好它:01 全景,04 对外 API,06 并行迭代器,最后 09 常见坑。
- 读懂调度器:02 线程池与 Registry,03 任务与 Latch,05 sleep 模块。
- 读懂迭代器:06 并行迭代器,07 slice 与排序。06 只依赖主线 1 的结论(「每次切分是一次
join_context,migrated()表示是否被偷或被注入」),可以跳过 02–05 直接读。
摘自 rayon 和 std 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。
前置知识
| 概念 | 本卷用到它的地方 | 已有的讲解 |
|---|---|---|
Chase-Lev 双端队列:Worker / Stealer,LIFO 与 FIFO | 每个 worker 的本地队列、广播队列 | Chase-Lev 双端队列(必读) |
Steal::Retry 的含义 | steal 循环、pop_injected_job | Steal、Retry 与批量窃取(必读) |
Injector 块链表 | 全局注入队列、JobFifo | Injector(必读) |
| 扩容与 epoch 回收 | JobRef 16 字节、每次扩容都 flush | 扩容与回收(必读) |
CachePadded、Backoff | sleep 状态数组;rayon 不用 Backoff 的对照 | CachePadded、Backoff |
Send / Sync,catch_unwind / resume_unwind | join 的约束、panic 怎样跨线程传回 | — |
Mutex + Condvar | LockLatch、睡眠 | — |
不需要懂 async:rayon 里没有 Future。和 tokio 的对照只出现在几处(spawn 的 panic、在 async 任务里调 par_iter)。
实验环境与方法
本机:Apple M2 Max(8 个性能核 + 4 个能效核,12 个逻辑核,available_parallelism() 报 12,所以默认全局池 12 个线程),macOS,rustc 1.99.1。生产服务器不做实验。
实验程序都在 exp-rayon/ 下,每个实验一个 crate,从同一个模板复制:rayon =1.12.0、rayon-core =1.13.0、crossbeam-deque =0.8.8、crossbeam-epoch =0.9.21、crossbeam-utils =0.8.23,对照用 tokio 1.54.1。一律离线编译(cargo --offline --release),[profile.release] debug = 1 方便采样。
规矩:
- 每个实验写成「断言 → 方法 → 结果 → 反向验证」。 反向验证是故意造一个「应该失败」或「应该看不到效果」的输入,确认测量方法真的能分辨。rayon 最常用的反向验证是
RAYON_NUM_THREADS=1:只有一个线程时没有窃取,凡是依赖窃取的现象都应消失,凡是确定性的计数都应固定。 - 性能类一律
--release,每组至少重复 3 次取中位数,页面上同时给出每一轮的数字。macOS 不能把线程绑到指定核上,线程可能落在能效核上,数字受调度影响。 - 记录负载。 计时开始前要求
sysctl -n vm.loadavg的 1 分钟负载低于 3,页面上写出测量时的负载。 - 正确性类(叶子数、报错原文、执行顺序、线程编号)在任何负载下都跑,至少 3 次,输出一致才写;不一致就把分布写出来。
- 实验输出里的本机路径一律去掉。
本卷的实验:
| # | 名字 | 在哪一页 |
|---|---|---|
| E1 | join 的粒度曲线(递归 fib 扫 cutoff) | join 与 join_context |
| E2 | 自适应切分的叶子数 | bridge 与自适应切分;一次 par_iter 引用 |
| E3 | 空闲 CPU、余温与唤醒延迟 | 05 sleep 模块 |
| E4 | with_min_len / with_max_len 对耗时的影响 | bridge 与自适应切分 |
| E5 | 在 tokio 任务里直接调 par_iter | spawn、常见坑 |
| E6 | 跨池 install 的执行顺序 | in_worker:从哪进池 |
| E7 | 持锁调嵌套 rayon 的重入 | join、常见坑 |
| E8 | collect 的两条路径、浮点求和 | collect 到 Vec |
| E9 | 并行排序的加速比 | 并行排序 |
| E10 | 全局池只能初始化一次 | 全局池与 Registry |
| E11 | join / scope / spawn 的 panic | spawn 与 spawn_fifo |
| E12 | 池内阻塞等待导致的饿死 | 09 常见坑 |
| E13 | 从池外进池的固定开销 | in_worker:从哪进池 |
页面上的标注:「推测」表示从源码或数据推出来、但没有实验或文档直接支撑的说法;性能数字都注明测量条件。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本页;源码地图 |
| 01 全景 | rayon 解决什么问题;一次 join、一次 par_iter、一个线程睡下又醒来 |
| 02 线程池与 Registry | ThreadPoolBuilder、全局池、主循环与找活顺序、三条进池路径 |
| 03 任务与 Latch | JobRef 与几种 Job;Latch 家族 |
| 04 对外 API | join、scope、spawn、broadcast、yield_now |
| 05 sleep 模块 | 本卷核心:计数器、睡与醒的握手、唤醒策略与实测 |
| 06 并行迭代器 | 两个 trait、plumbing、切分、适配器、collect、fold、par_bridge |
| 07 slice 与排序 | slice producer、并行排序 |
| 08 设计取舍 | 各章取舍汇总 |
| 09 常见坑 | 报错原文或现象做标题 |
| 10 在本站中 | 本站经 tantivy、image 间接依赖的 rayon |
| 11 动手 | 最小工作窃取池、最小 join、最小自适应 Splitter |
小结
- rayon 1.12.0 的 tag
v1.12.0同时覆盖 rayon-core 1.13.0:rayon-core-v1.13.0是它的祖先,rayon-core/目录零差异。全卷链接都用v1.12.0。 - 四条主线:一次 join(rayon-core)、一次 par_iter(rayon)、一个线程睡下又醒来(sleep 模块)、内存序与「设置之后不能再碰」。
- 前置是 crossbeam 卷 05 deque 的四页;本卷不重讲 deque 算法。
- 实验在 M2 Max(8P + 4E)上做,性能类 release、≥ 3 次取中位数、记录负载,每个实验都有反向验证,最常用的是
RAYON_NUM_THREADS=1。