Rust in Everything
基于 loom 0.7.2

loom 源码解析

全卷对着一个 tag 写:

crate版本源码链接用的 tag
loom0.7.2v0.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 从末尾往前找下一个选项。
  • 一次 Relaxed load 能读到什么:在最近 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,并写出测量时的负载。每个实验带一个反向验证。细节见 怎么读这一卷。

各部分

  1. 00 导读:版本、四条主线、阅读路线、实验方法;两层源码、每个文件管什么、feature、缺的 API。
  2. 01 全景:loom 解决什么问题、和压力测试 / Miri / TSan 的分工;一次 model 的 6 个交错、一次 load 的候选,各走一遍。
  3. 02 运行时与调度:本卷的基础。Builder 与主循环、generator 协程、Execution 与对象仓库、Path 的重放与回溯、DPOR、抢占上限与用户剪枝。
  4. 03 内存模型:向量时钟与同步点、写历史与 coherence、load 的候选、RMW 与释放序列、fence 与 SeqCst。
  5. 04 检测器:UnsafeCell 与原子的非同步访问、泄漏、死锁与分支上限、位置信息。
  6. 05 同步原语的模型:Mutex 与 RwLock、Condvar / Notify / park、线程与 lazy_static / thread_local、Arc / mpsc / future。
  7. 06 用法与调试:cfg(loom) 切换层、写一个好模型、checkpoint 与日志。
  8. 07 能力边界:假阳性与假阴性一览、SeqCst 与 load buffering、「loom 通过」意味着什么。
  9. 08 设计取舍:各章取舍汇总。
  10. 09 常见坑:用报错原文或现象做标题。
  11. 10 在本站中:本站依赖树里带 loom 测试的 crate。
  12. 11 动手:最小模型检查器;给一个小库加 loom。

从「导读」开始。只想会用的读者,读 01、06、09;想知道「loom 通过」意味着什么的,读 03 和 07;想读懂调度与 DPOR 的,读 02。

💬 关联讨论

发起讨论
加载中...