Rust in Everything
基于 loom 0.7.2

怎么读这一卷

主线位置:四条主线的总览。本页说明全卷对着哪一份源码写、四条主线各经过哪些文件和章节、按什么顺序读,以及实验在什么机器上、按什么规矩做。
flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|只想会用| u["01 全景 → 06 用法与调试 → 09 常见坑"]
  q -->|想知道「loom 通过」意味着什么| m["03 内存模型 → 07 能力边界"]
  q -->|读懂调度与 DPOR| s["02 运行时与调度"]
  q -->|全部| all["00 到 11 按顺序"]

锁定版本

项值
crateloom 0.7.2
tagv0.7.2,指向 a7033ee「Prepare v0.7.2 release (#348)」,2024-04-23
Cargo.tomlversion = "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 行。逐个看过:

提交标题改了什么
05885ecAdd checkpoint feature flag in the docs example (#355)lib.rs 文档里的命令加 --features checkpoint
6b42bbb、31dfebdFix typo (#357)、Fix doc link typo in hint.rs (#360)文档
be150d6Bump MSRV to 1.73 (#364)MSRV
bf80a9cMigrate to Rust 2021 (#365)edition 2021(去掉 use std::convert::TryInto 等)
b810b4aUse std::pin::pin! macro instead of pin-utils (#366)block_on 用 std::pin::pin!,不再依赖 pin-utils
3ef62e2Document what effect hints have on loom (#358)yield_now、spin_loop 的文档补例子(rt/mod.rs +89 行几乎全是文档)
0624ff3Fix build error on not 64 bit arch (#370)AtomicU64 / AtomicI64 改按 cfg(target_has_atomic = "64") 导出
850facaDocument Notify limitation (#373)Notify 文档补「同一时刻只能一个线程等待」
2dc2327、dbf32b0Fix discord link (#380)、Add license badge in README (#381)README
6abd413add ?Sized trait bound to RwLock (#401)RwLock<T: ?Sized>
948c8cctests: 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-244model 与 Builder
check:建 Execution / Scheduler;循环:每 checkpoint_interval 轮写 checkpoint、查上限 → run → 查泄漏 → stepmodel.rs:136-219同上
Scheduler::run:闭包包成 0 号协程,tick 当前活动线程rt/scheduler.rs:77-121协程调度器
每个 loom 操作先 branch:登记 operation → schedule() → 必要时 switch()rt/mod.rs:116-135Execution 与对象仓库
Execution::schedule:DPOR 登记回溯、尽量不抢占地选线程、死锁断言rt/execution.rs:135-256DPOR
Path::branch_thread:重放已有记录或新建 Schedulert/path.rs:240-334Path
一轮结束:Path::step 从末尾往前找下一个选项,截断rt/path.rs:389-467同上

全程:一次 model 的一生。

主线 2:一次 Relaxed load 能读到什么。

一步源码本卷
rt::Atomic::load:branch(Load) → synchronize → 走到头时算候选、push_load → branch_loadrt/atomic.rs:227-249一次 load 的候选
match_load_to_stores:在最近 7 次写里按 coherence 排除rt/atomic.rs:714-779load 的候选
State::load:竞争检查、apply_load_coherence、first_seen、sync_loadrt/atomic.rs:435-454写历史与 coherence
Synchronize::sync_load:只有 Acquire 及以上才并入对方时钟rt/synchronize.rs:23-37向量时钟与同步点

全程:一次 load 的候选。

主线 3:一次数据竞争是怎么被抓到的。

一步源码本卷
UnsafeCell::with_mut → rt::Cell::start_writecell/unsafe_cell.rs、rt/cell.rs:70UnsafeCell 与原子的非同步访问
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 能力边界。

阅读路线

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

与前几卷的关系

loom 在前几卷里已经被用过很多次、被介绍过两次。本卷不重讲「loom 是什么、怎么接」,而是打开它:前几卷里那些数字(Chase-Lev 的 65 个交错、crossbeam-epoch 的 10 374 个交错、「第 21 个交错就报错」)是怎么来的,loom 的「通过」到底说明了什么。

前几卷的页面已讲清的内容本卷
crossbeam:怎么验证并发代码loom 是什么;crossbeam 的切换层;compiler_fence 用 fence 代替;CI 用上限 2Execution 与对象仓库 解释 loom 原子「内存表示不同」;抢占上限与剪枝 解释上限 2 剪掉了什么
crossbeam:Chase-Lev65 个交错;去掉 SeqCst fence 第一个交错就重复取出fence 与 SeqCst
crossbeam:回收器10 374 个交错;unprotected() 第 21 个交错报错线程、线程局部与 lazy_static
crossbeam:最小 EBR188 / 3 727 个交错;一个没被抓到的改动抢占上限与剪枝、「loom 通过」意味着什么
tokio:源码地图 最后一节crate::loom 切换层cfg(loom) 切换层
bytes:clone 与提升src/loom.rs;两个并发 clone 的 loom 测试同上

前置知识

概念本卷用到它的地方已有的讲解
Acquire / Release / Relaxed / SeqCst 的配对主线 2、整个 03crossbeam 卷:内存序(必读)
「x86 上测不出、要靠工具」01crossbeam 卷:怎么验证并发代码(必读)
向量时钟(happens-before 的偏序表示)02 DPOR、03本卷 向量时钟与同步点 从头讲
深度优先搜索与回溯02 Path—
std::sync::Mutex / Condvar 的用法05parking_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抢占上限与交错数、耗时抢占上限与剪枝、写一个好模型
E3checkpoint 文件何时写、能否重放checkpoint、日志与位置
E4max_permutations 的检查粒度model 与 Builder
E5litmus:SB、LB、CoRRfence 与 SeqCst、SeqCst 与 load buffering
E6每个原子只留 7 次写一次 load 的候选、写历史与 coherence
E7UnsafeCell 并发写UnsafeCell 与原子的非同步访问
E8泄漏泄漏检测
E9Condvar 的 if 等待与 wait_timeoutCondvar、Notify 与 park
E10死锁与 abort死锁、分支上限与位置信息
E11不 yield 的自旋同上
E12线程数上限model 与 Builder
E13lazy_static! 与 thread_local!线程、线程局部与 lazy_static
E14RwLock 同线程嵌套读Mutex 与 RwLock
E15子线程 panic线程、线程局部与 lazy_static
E16compare_exchange_weak 不伪失败RMW 与释放序列
E17stop_exploring / skip_branch抢占上限与剪枝
E18block_on 与 AtomicWaker 的丢唤醒Arc、mpsc 与 future
E19LOOM_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 的实验分进程跑。

💬 关联讨论

发起讨论
加载中...