loom 解决什么问题
主线位置:四条主线的起点。本页说明 loom 要对付的问题和它的基本做法;主线 1(一次 model 的一生)与主线 2(一次 load 的候选)各有一页全景,主线 3(数据竞争的检测)与主线 4(看到什么、看不到什么)在本页给出轮廓。
flowchart LR bug["一个并发错误"] --> rare["只在极少数交错下出现:<br/>循环跑一百万次也许一次"] bug --> hw["只在弱内存序的硬件上出现:<br/>x86 上跑多少次都没有"] rare --> loom["loom:同一个闭包重放多遍,<br/>每遍系统地换一种交错"] hw --> loom2["loom:在软件里模拟 C11 内存模型,<br/>一次 load 能读到的每个值都试一遍"] loom --> rep["失败可以确定性重放(Path / checkpoint)"] loom2 --> rep
两类难题
crate 文档的开头(lib.rs:11-60)把问题分成两层:
- 交错太多,坏的那个太少。 一个线程 spawn 另一个线程后立刻读一个值,绝大多数时候被 spawn 的线程还没来得及写,断言成立;偶尔它恰好先跑,断言失败。传统做法是循环跑、或者给机器加负载,「hope that an execution fails」。
- 有些错误在某些硬件上根本不会出现。 用了比
SeqCst弱的内存序时,错误可能只在某种内存模型的 CPU 上出现,「no amount of iteration will demonstrate the bug on different hardware」。crossbeam 卷:怎么验证并发代码 讲过同一个问题:一个本该是Acquire却写成Relaxed的读,在 x86 上被硬件兜住,crossbeam 的 CI 要靠 arm64、armv7、powerpc64le 机器才有机会暴露。
第一类问题有多罕见,可以直接量一下。文档里那个例子(主线程 spawn 一个线程去 store(1),自己立刻 load 并断言读到 0)用 std 线程跑 10 万次(exp-loom/runtime/tests/doc_example.rs 的 std_loop,release,2026-11-03 00:21,开始时负载 1 分钟 2.08、5 分钟 2.92),三轮里断言失败的次数分别是 16、13、8,也就是大约万分之一。规划阶段在负载 3–5 时跑过一次,是 50 次:这个比例取决于此刻机器上有多少空闲核、新线程多快被调度,换一台机器、换一个负载就是另一个数。要靠它来发现错误,只能寄希望于跑得足够多。
loom 的做法
Loom provides a way to deterministically explore the various possible execution permutations without relying on random executions.(
lib.rs:18-19)
两件事:
- 确定性重放。 loom 自己当调度器:所有模型线程是同一个 OS 线程上的协程,什么时候切换完全由 loom 决定(协程调度器)。每一轮从头跑一遍闭包,只记录「每个分支点选了什么」(Path),同样的记录必然得到同样的执行。失败可以原样复现,路径可以存成文件(checkpoint)。
- 系统地枚举。 第一轮按「每个线程尽量一口气跑到底」走;运行中凡是两个线程访问同一个对象、且二者的先后会影响结果的地方,记一个「这里可以换另一个线程先跑」(DPOR);每次原子 load 列出按 C11 规则可能读到的那几次写(一次 load 的候选)。一轮结束,深度优先地换一个还没试过的选项,再跑一轮。
文档的例子改用 loom 类型、包进 loom::model 后,第 3 个交错就断言失败(同一个实验文件的 doc_example)。上面 std 线程要碰运气的事,在 loom 里是确定的:每次运行都在第 3 个交错失败。
这样做要付的代价,第一是侵入:被测代码必须用 loom 的类型(loom::sync::atomic::AtomicUsize 而不是 std 的),loom 看不到它没接管的东西(lib.rs:253-269),所以库要写一层 cfg(loom) 切换(cfg(loom) 切换层)。第二是规模:交错数随线程数和操作数增长很快,loom 把模型限制在 5 个线程以内(MAX_THREADS),还提供抢占上限来裁剪(抢占上限与剪枝)。第三是内存模型的取舍:loom 没有实现完整的 C11 模型,README 自己列了两条「Unsupported features」,本卷在 07 能力边界 汇总。
和其他办法对照
只写本站已经实测或从代码能确认的内容;空着的格子表示本站没有证据。
| 压力测试(std 线程反复跑) | loom | Miri | ThreadSanitizer | |
|---|---|---|---|---|
| 怎么跑 | 真线程、真硬件 | 被测代码换成 loom 类型,单 OS 线程上的协程,loom 选每一步跑谁、每次 load 读哪次写 | 解释执行 MIR | 编译器插桩,真线程 |
| 能发现什么 | 恰好跑到的交错里断言失败 | 模型里断言失败;UnsafeCell 与原子 with_mut 的非同步访问(「Causality violation」);loom Arc / alloc 泄漏;死锁(04 检测器) | 未定义行为(越界、释放后使用、别名规则);crossbeam 的 CI 关掉抢占才能让 deque 的测试通过,脚本注释说「this code technically has UB and Miri catches that」(crossbeam 卷) | 数据竞争 |
| 弱内存序 | 取决于硬件:本机 M2(aarch64)上能出现的重排才能出现 | 按 C11 规则枚举 load 的候选,但 SeqCst 访问按 AcqRel 处理、不探索 load buffering、每个原子只留 7 次写(07 能力边界) | 不理解独立的 fence:crossbeam 在 TSan 下把「Relaxed + fence」改写成 Acquire / Release 访问,否则一堆假阳性(crossbeam 卷);bytes 为它把 fence 改成 Acquire load(bytes 卷) | |
| 看不到什么 | 没跑到的交错 | 没换成 loom 类型的一切:std 原子、parking_lot_core、系统调用、FFI(lib.rs:253-269) | ||
| 要改代码吗 | 不用 | 要:cfg(loom) 切换层,UnsafeCell 的 with / with_mut 等 API 差别 | 不用;但慢的测试要在 Miri 下缩小规模(bytes 的 if cfg!(miri) { 16 } else { 256 },bytes 卷:源码地图) | 不用;要 nightly 的 -Z sanitizer=thread 与 -Z build-std(crossbeam、bytes 的 CI) |
| 失败能重放吗 | 不能 | 能:Path 存成 JSON(Path) |
这几种工具不互相替代。本站各卷里的库都是组合使用:crossbeam 的 CI 有跨架构测试、loom(只覆盖 epoch)、Miri、三种 sanitizer;bytes 的 CI 有 Miri、ASan、TSan、loom(两个并发 clone 的模型)。loom 的位置是:在被测代码愿意改造的那一小块上,把调度和读值的选择交给程序来枚举。
理论出处:CDSChecker
crate 文档的最后一节说 loom 实现的是 CDSChecker 论文里的技术(「CDSChecker: Checking Concurrent Data Structures Written with C/C++ Atomics」),README 也链到同一篇。两处链接不同:
| 出处 | 链接指向 |
|---|---|
lib.rs 的 [cdschecker](lib.rs:337-347) | 一所大学个人主页上的 PDF,文件名 c11modelcheck.pdf |
README.md 的 [cdschecker] | 同一所大学一个研究组网站上的 PDF,文件名 toplas16.pdf |
本卷写作时没有联网核对两个文件的内容;从文件名看,前者是会议版本,后者是 2016 年 TOPLAS 期刊上的扩展版。本卷不讲论文的形式化,只看 loom 实现了它的哪些规则:rt/atomic.rs 开头 55 行把论文图 7、9、10 的 coherence 规则逐条列出,其中几条(SeqCst 相关、RMW 原子性)下面是空的,写历史与 coherence 逐条标出实现在哪、哪些没实现。DPOR 与抢占上限也是模型检查领域的通用技术,loom 的实现在 02 运行时与调度。
谁在用
本站前几卷里已经多次出现 loom:
| 库 | 怎么用 | 本站页面 |
|---|---|---|
| tokio | src/loom/ 切换层,cfg(all(test, loom)) 时用 loom;大量内部模块有 loom 测试,loom 下把队列容量等常数调小 | tokio 卷:源码地图 最后一节 |
| bytes | src/loom.rs 切换原子类型;两个并发 clone 的 loom 测试;Bytes::new 在 loom 下要另写一份非 const 的 | bytes 卷:clone 与提升 |
| crossbeam-epoch | mod primitive 切换层;tests/loom.rs 两个模型,CI 用抢占上限 2 | crossbeam 卷:怎么验证并发代码、回收器 |
| 本站自己的实验 | crossbeam 卷用 loom 检查 Chase-Lev 的 fence 与最小 EBR | Chase-Lev、最小 EBR |
反过来,parking_lot 卷的动手部分写明了「parking_lot_core 用真实的原子和系统调用,不能放进 loom 的模型里」(parking_lot 卷:动手),那里的证据只有压力测试加反向验证。本站依赖树里还有哪些 crate 带 loom 测试、本站自己的代码是否值得加,见 10 在本站中。
小结
- loom 对付两类问题:罕见的交错,以及只在弱内存序硬件上出现的重排。
- 做法:自己当调度器(单 OS 线程上的协程),每轮从头重放闭包;DPOR 决定在哪里换序,C11 规则决定 load 的候选;失败可以确定性重放。
- 文档的例子:std 线程跑 10 万次,失败 8–16 次(负载 < 3),且随负载变化;换成 loom,每次都在第 3 个交错失败。
- 代价:侵入式(只看得到 loom 类型)、规模(5 个线程、抢占上限)、内存模型不完整(07 汇总)。
- 和压力测试、Miri、TSan 互补,不互相替代;理论出处是 CDSChecker,
lib.rs与 README 链了两个不同的版本。