loom 源码解析
全卷对着一个 tag 写:
| crate | 版本 | 源码链接用的 tag |
|---|---|---|
loom | 0.7.2 | v0.7.2(提交 a7033ee,「Prepare v0.7.2 release (#348)」,2024-04-23) |
0.7.2 是目前最新的发布版。之后主分支上的 13 个提交只改了文档、MSRV、edition 和两处 API 的放宽,没有改动调度、内存模型或检测逻辑,差别列在 怎么读这一卷 的一张表里。
loom 是一个以库的形式提供的并发模型检查器。被测代码把 std::sync::atomic、Mutex、thread::spawn、UnsafeCell 等换成 loom 的同名类型(通常靠一层 #[cfg(loom)] 切换),测试写成 loom::model(|| { ... })。loom 在一个 OS 线程上用协程模拟最多 5 个线程,反复运行同一个闭包,每次走一条不同的线程交错和读值选择,选择记在一条路径里,按深度优先回溯。它做三件事:
- 调度空间:每个原子操作、锁操作、
Arc计数操作、channel 操作之前都是一个分支点;DPOR(动态偏序归约)只在相互依赖的访问之间登记「这里可以换另一个线程先跑」;抢占上限(LOOM_MAX_PREEMPTIONS)进一步裁剪。 - 弱内存序:每个原子变量保存最近 7 次写,一次 load 按 coherence 规则算出可能读到哪些写,每个候选都是一个分支;
Acquire/Release/ fence 用每线程的向量时钟传递因果关系。 - 检测器:
UnsafeCell与原子with_mut的非同步访问(「Causality violation」)、Arc/alloc/Track泄漏、死锁(没有可运行的线程但有线程没结束)、分支数超限(常见于不yield_now的自旋)。
它和前几卷的关系:loom 在前几卷里被用过很多次(crossbeam 卷 的 Chase-Lev、回收器、最小 EBR 三个实验),被介绍过两次(crossbeam 卷:怎么验证并发代码、tokio 卷:源码地图 最后一节)。本卷不重讲「loom 是什么、怎么接」,而是打开它:那些「65 个交错」「第 21 个交错就报错」是怎么来的,loom 的「通过」说明了什么、没说明什么。bytes 卷 的切换层、parking_lot 卷 里「parking_lot_core 不能放进 loom」的那句话,在本卷都有对应的解释。
全卷沿四条线展开:
- 一次
loom::model的一生:Builder读环境变量 → 每轮把闭包包成 0 号协程 → 每个 loom 操作前schedule():DPOR 登记回溯、尽量不抢占地选线程 → 路径重放或新建分支 → 一轮结束step从末尾往前找下一个选项。 - 一次
Relaxedload 能读到什么:在最近 7 次写里排除「更新的写已在本线程因果历史里」的旧写,剩下的逐个作为分支;只有Acquire及以上才把对方的时钟并进来。 - 一次数据竞争是怎么被抓到的:
UnsafeCell访问不是调度点,靠向量时钟比较当场判定,不靠「恰好交错到一起」。 - 横线:看到什么、看不到什么:只看得到换成 loom 类型的操作;看得到的部分也有建模上的取舍(SeqCst 访问按 AcqRel 处理、不探索 load buffering、7 次写窗口、
compare_exchange_weak不伪失败、Condvar不虚假唤醒、最多 5 个线程),每条都说清是假阳性还是假阴性方向。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> rt[02 运行时与调度] rt --> mm[03 内存模型] rt --> ck[04 检测器] rt --> prim[05 同步原语的模型] mm --> usage[06 用法与调试] ck --> usage prim --> usage usage --> limits[07 能力边界] limits --> design[08 设计取舍] design --> pit[09 常见坑] pit --> site[10 在本站中] site --> hands[11 动手]
实验环境
本机 Apple M2 Max(8 个性能核 + 4 个能效核),macOS,rustc 1.99.1。实验程序在 exp-loom/ 下,依赖锁定 loom =0.7.2(打开 checkpoint 与 futures feature),一律 RUSTFLAGS="--cfg loom" cargo test --release --offline,每个实验用自己的 target 目录。loom 只用一个 OS 线程,交错数是确定的,与核数、负载无关,页面上的「第 N 个交错」「共 N 个交错」任何时候都能复现;只有耗时受负载影响,计时要求 1 分钟与 5 分钟负载都低于 3,并写出测量时的负载。每个实验带一个反向验证。细节见 怎么读这一卷。
各部分
- 00 导读:版本、四条主线、阅读路线、实验方法;两层源码、每个文件管什么、feature、缺的 API。
- 01 全景:loom 解决什么问题、和压力测试 / Miri / TSan 的分工;一次 model 的 6 个交错、一次 load 的候选,各走一遍。
- 02 运行时与调度:本卷的基础。
Builder与主循环、generator 协程、Execution与对象仓库、Path的重放与回溯、DPOR、抢占上限与用户剪枝。 - 03 内存模型:向量时钟与同步点、写历史与 coherence、load 的候选、RMW 与释放序列、fence 与 SeqCst。
- 04 检测器:
UnsafeCell与原子的非同步访问、泄漏、死锁与分支上限、位置信息。 - 05 同步原语的模型:
Mutex与RwLock、Condvar/Notify/ park、线程与lazy_static/thread_local、Arc/ mpsc / future。 - 06 用法与调试:
cfg(loom)切换层、写一个好模型、checkpoint 与日志。 - 07 能力边界:假阳性与假阴性一览、SeqCst 与 load buffering、「loom 通过」意味着什么。
- 08 设计取舍:各章取舍汇总。
- 09 常见坑:用报错原文或现象做标题。
- 10 在本站中:本站依赖树里带 loom 测试的 crate。
- 11 动手:最小模型检查器;给一个小库加 loom。
从「导读」开始。只想会用的读者,读 01、06、09;想知道「loom 通过」意味着什么的,读 03 和 07;想读懂调度与 DPOR 的,读 02。