怎么读这一卷
主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|只想会用| u["01 全景 → 06 用法与调试 → 09 常见坑"]
q -->|想知道「loom 通过」意味着什么| m["03 内存模型 → 07 能力边界"]
q -->|读懂调度与 DPOR| s["02 运行时与调度"]
q -->|全部| all["00 到 11 按顺序"]
锁定版本
| 项 | 值 |
|---|---|
| crate | loom 0.7.2 |
| tag | v0.7.2,指向 a7033ee「Prepare v0.7.2 release (#348)」,2024-04-23 |
Cargo.toml | version = "0.7.2"、edition = "2018"、rust-version = "1.65" |
| 仓库 | 第一个提交 2b37a35「Initial commit」,2018-12-15;到 v0.7.2 共 222 个提交 |
| tag 数 | 37 个,v0.1.0 到 v0.7.2 |
0.7.2 是目前最新的发布版。crates.io 上的包和仓库一致:把 registry 里解压的 loom-0.7.2/src 和工作区 src/ 做 diff -r,输出为空。全卷的源码链接统一用 tag v0.7.2,形如 https://github.com/tokio-rs/loom/blob/v0.7.2/src/rt/execution.rs#L136-L256,行号从 v0.7.2 的工作区取。
CHANGELOG 里有 0.5.3(2021-11-23)和 0.5.4(2021-12-03)两节,tag 里没有 v0.5.3、v0.5.4。两个版本的发布提交可以从 Cargo.toml 的历史里找到:5f0a1ab「chore: prepare loom 0.5.3 (#241)」与 5450fb9「chore: prepare to release 0.5.4 (#244)」。本卷引用这两个版本的改动时写提交号和 PR 号。
v0.7.2 之后
v0.7.2..948c8cc(2024-05 到 2026-02)共 13 个提交,src/ 下 11 个文件 +152 / −31 行。逐个看过:
| 提交 | 标题 | 改了什么 |
|---|---|---|
05885ec | Add checkpoint feature flag in the docs example (#355) | lib.rs 文档里的命令加 --features checkpoint |
6b42bbb、31dfebd | Fix typo (#357)、Fix doc link typo in hint.rs (#360) | 文档 |
be150d6 | Bump MSRV to 1.73 (#364) | MSRV |
bf80a9c | Migrate to Rust 2021 (#365) | edition 2021(去掉 use std::convert::TryInto 等) |
b810b4a | Use std::pin::pin! macro instead of pin-utils (#366) | block_on 用 std::pin::pin!,不再依赖 pin-utils |
3ef62e2 | Document what effect hints have on loom (#358) | yield_now、spin_loop 的文档补例子(rt/mod.rs +89 行几乎全是文档) |
0624ff3 | Fix build error on not 64 bit arch (#370) | AtomicU64 / AtomicI64 改按 cfg(target_has_atomic = "64") 导出 |
850faca | Document Notify limitation (#373) | Notify 文档补「同一时刻只能一个线程等待」 |
2dc2327、dbf32b0 | Fix discord link (#380)、Add license badge in README (#381) | README |
6abd413 | add ?Sized trait bound to RwLock (#401) | RwLock<T: ?Sized> |
948c8cc | tests: Fix typo: mutal -> mutual (#404) | 测试名 |
没有一个提交改动调度、路径、内存模型或检测逻辑。 所以正文只写 0.7.2,这张表就是全部差别;遇到相关的地方(例如 Notify 只允许一个等待者、RwLock 的 ?Sized),在对应章提一句。
四条主线
每一页开头都会标出本页在哪条主线上。
主线 1:一次 loom::model 的一生。
| 一步 | 源码 | 本卷 |
|---|---|---|
model(f) 装 tracing 订阅者;Builder::new() 读 8 个环境变量 | model.rs:74-128、228-244 | model 与 Builder |
check:建 Execution / Scheduler;循环:每 checkpoint_interval 轮写 checkpoint、查上限 → run → 查泄漏 → step | model.rs:136-219 | 同上 |
Scheduler::run:闭包包成 0 号协程,tick 当前活动线程 | rt/scheduler.rs:77-121 | 协程调度器 |
每个 loom 操作先 branch:登记 operation → schedule() → 必要时 switch() | rt/mod.rs:116-135 | Execution 与对象仓库 |
Execution::schedule:DPOR 登记回溯、尽量不抢占地选线程、死锁断言 | rt/execution.rs:135-256 | DPOR |
Path::branch_thread:重放已有记录或新建 Schedule | rt/path.rs:240-334 | Path |
一轮结束:Path::step 从末尾往前找下一个选项,截断 | rt/path.rs:389-467 | 同上 |
全程:一次 model 的一生。
主线 2:一次 Relaxed load 能读到什么。
| 一步 | 源码 | 本卷 |
|---|---|---|
rt::Atomic::load:branch(Load) → synchronize → 走到头时算候选、push_load → branch_load | rt/atomic.rs:227-249 | 一次 load 的候选 |
match_load_to_stores:在最近 7 次写里按 coherence 排除 | rt/atomic.rs:714-779 | load 的候选 |
State::load:竞争检查、apply_load_coherence、first_seen、sync_load | rt/atomic.rs:435-454 | 写历史与 coherence |
Synchronize::sync_load:只有 Acquire 及以上才并入对方时钟 | rt/synchronize.rs:23-37 | 向量时钟与同步点 |
全程:一次 load 的候选。
主线 3:一次数据竞争是怎么被抓到的。
| 一步 | 源码 | 本卷 |
|---|---|---|
UnsafeCell::with_mut → rt::Cell::start_write | cell/unsafe_cell.rs、rt/cell.rs:70 | UnsafeCell 与原子的非同步访问 |
track_write:上一次读 / 写的时钟有分量领先于本线程,就是并发访问 | rt/cell.rs:119 | 同上 |
location::panic(...).fire():「Causality violation: …」 | rt/location.rs:89-115 | 死锁、分支上限与位置信息 |
单元格访问不是调度点,竞争靠向量时钟当场判定,不靠「恰好交错到一起」。没有单独的全景页,一次 model 的一生 的「换成 Relaxed」一节是它的一个实例。
主线 4:横线,loom 看到什么、看不到什么。 不是一条调用链,而是每章都要回答的问题:loom 只看得到换成 loom 类型的操作(lib.rs:253-269);看得到的部分也有建模上的取舍,例如 SeqCst 访问按 AcqRel 处理、不探索 load buffering、每个原子只留 7 次写、compare_exchange_weak 不会伪失败、Condvar 不会虚假唤醒、最多 5 个线程。每遇到一条,就说清它是「loom 报错但代码其实对」(假阳性)还是「代码有错但 loom 通过」(假阴性)方向。汇总在 07 能力边界。
阅读路线
- 只想会用:01 全景,06 用法与调试,09 常见坑。遇到「设多少
LOOM_MAX_PREEMPTIONS」时读 抢占上限与剪枝。 - 想知道「loom 通过」意味着什么:03 内存模型,07 能力边界。
- 读懂调度与 DPOR:02 运行时与调度 按顺序读;03–05 只用到 02 的结论(「每个 loom 操作前一个
Schedule」「load 多一个Load分支」「阻塞就是把自己标成Disabled再调度」)。
摘自 loom 的代码片段保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的实验代码用 2 空格缩进。
与前几卷的关系
loom 在前几卷里已经被用过很多次、被介绍过两次。本卷不重讲「loom 是什么、怎么接」,而是打开它:前几卷里那些数字(Chase-Lev 的 65 个交错、crossbeam-epoch 的 10 374 个交错、「第 21 个交错就报错」)是怎么来的,loom 的「通过」到底说明了什么。
| 前几卷的页面 | 已讲清的内容 | 本卷 |
|---|---|---|
| crossbeam:怎么验证并发代码 | loom 是什么;crossbeam 的切换层;compiler_fence 用 fence 代替;CI 用上限 2 | Execution 与对象仓库 解释 loom 原子「内存表示不同」;抢占上限与剪枝 解释上限 2 剪掉了什么 |
| crossbeam:Chase-Lev | 65 个交错;去掉 SeqCst fence 第一个交错就重复取出 | fence 与 SeqCst |
| crossbeam:回收器 | 10 374 个交错;unprotected() 第 21 个交错报错 | 线程、线程局部与 lazy_static |
| crossbeam:最小 EBR | 188 / 3 727 个交错;一个没被抓到的改动 | 抢占上限与剪枝、「loom 通过」意味着什么 |
| tokio:源码地图 最后一节 | crate::loom 切换层 | cfg(loom) 切换层 |
| bytes:clone 与提升 | src/loom.rs;两个并发 clone 的 loom 测试 | 同上 |
前置知识
| 概念 | 本卷用到它的地方 | 已有的讲解 |
|---|---|---|
| Acquire / Release / Relaxed / SeqCst 的配对 | 主线 2、整个 03 | crossbeam 卷:内存序(必读) |
| 「x86 上测不出、要靠工具」 | 01 | crossbeam 卷:怎么验证并发代码(必读) |
| 向量时钟(happens-before 的偏序表示) | 02 DPOR、03 | 本卷 向量时钟与同步点 从头讲 |
| 深度优先搜索与回溯 | 02 Path | — |
std::sync::Mutex / Condvar 的用法 | 05 | parking_lot 卷 有对照 |
不需要懂协程的实现;loom 用 generator crate 的有栈协程,本卷只讲它怎么用(协程调度器)。
实验环境与方法
本机:Apple M2 Max(8 个性能核 + 4 个能效核),macOS,rustc 1.99.1。生产服务器不做实验。
实验程序都在 exp-loom/ 下:模板 exp-loom/base(loom =0.7.2,打开 checkpoint 与 futures feature;另有 crossbeam-utils、parking_lot 0.12.5、tokio、futures 作对照),各实验复制它;规划阶段的验证在 exp-loom/survey/。一律 RUSTFLAGS="--cfg loom" cargo test --release --offline。规矩和 crossbeam 卷、parking_lot 卷 相同(断言 → 方法 → 结果 → 反向验证;计时要求 1 分钟与 5 分钟负载都低于 3),另有四条 loom 特有的:
- loom 只用一个 OS 线程。 模型里的「线程」都是同一个 OS 线程上的协程,所以交错数是确定的:同一个模型、同样的上限,任何机器、任何负载下都是同一个数。页面上写「第 N 个交错失败」「共 N 个交错」不需要注明负载;只有耗时受负载影响,耗时一律注明。
- 单独的 target 目录。
--cfg loom写在RUSTFLAGS里,会改变整个依赖图的编译指纹,和平时的 target 目录共用会来回重编。每个实验用自己目录下的CARGO_TARGET_DIR。 - 会 abort 的实验分进程跑。 死锁、嵌套读等实验在 panic 之后的展开过程中会 abort(死锁、分支上限与位置信息),一个 abort 会带走整个测试进程,所以这类实验每个
#[test]一个场景,逐个--exact运行。 - 只加打印的副本。 要看每一轮的完整路径时,用
exp-loom/pathdump/里从 registry 复制的 loom 0.7.2,只加了由环境变量控制的println!,算法不变;交错数与原版逐一核对。用到它的页面会注明。
本卷的实验:
| # | 名字 | 在哪一页 |
|---|---|---|
| E1 | 消息传递:Release / Acquire 通过,Relaxed 被抓到 | 一次 model 的一生、一次 load 的候选、UnsafeCell 与原子的非同步访问 |
| E2 | 抢占上限与交错数、耗时 | 抢占上限与剪枝、写一个好模型 |
| E3 | checkpoint 文件何时写、能否重放 | checkpoint、日志与位置 |
| E4 | max_permutations 的检查粒度 | model 与 Builder |
| E5 | litmus:SB、LB、CoRR | fence 与 SeqCst、SeqCst 与 load buffering |
| E6 | 每个原子只留 7 次写 | 一次 load 的候选、写历史与 coherence |
| E7 | UnsafeCell 并发写 | UnsafeCell 与原子的非同步访问 |
| E8 | 泄漏 | 泄漏检测 |
| E9 | Condvar 的 if 等待与 wait_timeout | Condvar、Notify 与 park |
| E10 | 死锁与 abort | 死锁、分支上限与位置信息 |
| E11 | 不 yield 的自旋 | 同上 |
| E12 | 线程数上限 | model 与 Builder |
| E13 | lazy_static! 与 thread_local! | 线程、线程局部与 lazy_static |
| E14 | RwLock 同线程嵌套读 | Mutex 与 RwLock |
| E15 | 子线程 panic | 线程、线程局部与 lazy_static |
| E16 | compare_exchange_weak 不伪失败 | RMW 与释放序列 |
| E17 | stop_exploring / skip_branch | 抢占上限与剪枝 |
| E18 | block_on 与 AtomicWaker 的丢唤醒 | Arc、mpsc 与 future |
| E19 | LOOM_LOG 的两个级别 | checkpoint、日志与位置 |
| E20 | 每秒交错数,release 与 debug | 写一个好模型 |
页面上的标注:「推测」表示从源码或数据推出来、但没有实验或文档直接支撑的说法。上游的 README、CHANGELOG、源码注释是资料,和代码不一致时以代码为准,页面里指出出入。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本页;源码地图 |
| 01 全景 | loom 解决什么问题;一次 model 的一生;一次 load 的候选 |
| 02 运行时与调度 | Builder、协程调度器、Execution、Path、DPOR、抢占上限 |
| 03 内存模型 | 向量时钟、写历史与 coherence、load 的候选、RMW、fence 与 SeqCst |
| 04 检测器 | UnsafeCell 竞争、泄漏、死锁与分支上限 |
| 05 同步原语的模型 | Mutex / RwLock、Condvar / Notify、线程与静态量、Arc / mpsc / future |
| 06 用法与调试 | 切换层、写一个好模型、checkpoint 与日志 |
| 07 能力边界 | 假阳性与假阴性一览;SeqCst 与 load buffering;「loom 通过」意味着什么 |
| 08 设计取舍 | 各章取舍汇总 |
| 09 常见坑 | 报错原文或现象做标题 |
| 10 在本站中 | 本站依赖树里带 loom 测试的 crate |
| 11 动手 | 最小模型检查器;给一个小库加 loom |
小结
- 全卷对着 tag
v0.7.2(a7033ee)写,registry 包与工作区零差异;之后 13 个提交只有文档、MSRV、edition 与两处 API 放宽,没有行为变化。 - 四条主线:一次 model 的一生、一次 load 的候选、一次数据竞争的检测、看到什么与看不到什么。
- 前置是 crossbeam 卷的内存序两页。
- loom 单 OS 线程运行,交错数确定、可在任何负载下复现;耗时注明负载;每个实验用单独的 target 目录,会 abort 的实验分进程跑。