Rust in Everything
基于 rayon 1.12.0

怎么读这一卷

主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
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
rayon1.12.02026-04-13(RELEASES.md)v1.12.0,提交 c9ced18(2026-04-14,「Merge pull request #1294」)
rayon-core1.13.02025-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:当前线程是不是 workerjoin/mod.rs、registry.rsin_worker:从哪进池
栈上建 StackJob{latch: SpinLatch, func, result},push 进本地 dequejob.rs、registry.rsJobRef 与几种 Job
跑 A;回头 take_local_job,弹到 B 就 run_inlinejoin/mod.rsjoin 与 join_context
B 被偷了:wait_until → wait_until_cold,边等边找活registry.rsWorkerThread 主循环
窃取者执行 B 后 Latch::set,必要时唤醒原线程latch.rsLatch 家族

全程时序图:一次 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_consumeriter/plumbing/mod.rsplumbing
LengthSplitter::try_split:切就 join_context,不切就 fold_with同上bridge 与自适应切分
回溯时 reducer.reduce(left, right)iter/sum.rsfold、reduce 与 sum

全程时序图:一次 par_iter。

主线 3:一个线程睡下又醒来。 sleep 模块,本卷核心。

一步源码本卷
start_looking:inactive 计数 +1sleep/mod.rs、sleep/counters.rs05 sleep 模块
no_work_found:32 轮 yield_now,announce_sleepy,再找一轮sleep/mod.rs同上
sleep:latch 进 SLEEPY / SLEEPING,查 JEC,登记 sleeping,最后看一眼注入队列,Condvar::waitsleep/mod.rs、latch.rs同上
发布方 new_jobs:翻 JEC,按计数叫醒几个sleep/mod.rs同上

全程状态图:一个线程睡下又醒来。

主线 4:内存序与「设置之后不能再碰」横线。 不是一条调用链,而是两个贯穿全卷的问题。一是每遇到一个 Ordering,问它和谁配对,分类沿用 crossbeam 卷的内存序一章。二是 rayon 特有的:Latch::set 之后,等待方可能立刻返回、销毁 latch 所在的栈帧,所以 set 的参数是 *const Self 而不是 &self,SpinLatch::set 要先把需要的字段读出来再置位。这一条在 Latch 家族 集中讲,其余各页遇到就点一下。

阅读路线

摘自 rayon 和 std 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。

前置知识

概念本卷用到它的地方已有的讲解
Chase-Lev 双端队列:Worker / Stealer,LIFO 与 FIFO每个 worker 的本地队列、广播队列Chase-Lev 双端队列(必读)
Steal::Retry 的含义steal 循环、pop_injected_jobSteal、Retry 与批量窃取(必读)
Injector 块链表全局注入队列、JobFifoInjector(必读)
扩容与 epoch 回收JobRef 16 字节、每次扩容都 flush扩容与回收(必读)
CachePadded、Backoffsleep 状态数组;rayon 不用 Backoff 的对照CachePadded、Backoff
Send / Sync,catch_unwind / resume_unwindjoin 的约束、panic 怎样跨线程传回—
Mutex + CondvarLockLatch、睡眠—

不需要懂 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 次,输出一致才写;不一致就把分布写出来。
  • 实验输出里的本机路径一律去掉。

本卷的实验:

#名字在哪一页
E1join 的粒度曲线(递归 fib 扫 cutoff)join 与 join_context
E2自适应切分的叶子数bridge 与自适应切分;一次 par_iter 引用
E3空闲 CPU、余温与唤醒延迟05 sleep 模块
E4with_min_len / with_max_len 对耗时的影响bridge 与自适应切分
E5在 tokio 任务里直接调 par_iterspawn、常见坑
E6跨池 install 的执行顺序in_worker:从哪进池
E7持锁调嵌套 rayon 的重入join、常见坑
E8collect 的两条路径、浮点求和collect 到 Vec
E9并行排序的加速比并行排序
E10全局池只能初始化一次全局池与 Registry
E11join / scope / spawn 的 panicspawn 与 spawn_fifo
E12池内阻塞等待导致的饿死09 常见坑
E13从池外进池的固定开销in_worker:从哪进池

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

目录

部分内容
00 导读本页;源码地图
01 全景rayon 解决什么问题;一次 join、一次 par_iter、一个线程睡下又醒来
02 线程池与 RegistryThreadPoolBuilder、全局池、主循环与找活顺序、三条进池路径
03 任务与 LatchJobRef 与几种 Job;Latch 家族
04 对外 APIjoin、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。

💬 关联讨论

发起讨论
加载中...