常见坑
每一页对着一个报错或一个现象:先给最小复现和实测输出,再到源码里找它从哪来,最后给出改法。机制在前面各章讲过的只链过去,能复用前面实验的直接引用数字。本部分新做的复现在 exp-loom/pitfalls/tests/pit.rs(19 个用例,每个 #[test] 用 run_each.sh 单独起一个进程跑,因为其中几个会 abort)。本机 M2 Max,macOS,rustc 1.99.1,loom =0.7.2,RUSTFLAGS="--cfg loom" cargo test --release --offline。交错数是确定的。
报错原文
- cannot access Loom execution state from outside a Loom model:在
model外构造或使用 loom 类型;把 loom 对象留到下一轮则报「failed to get object for ref」(本部分新测)。 - Model exceeded maximum number of branches:不 yield 的自旋(E11,调大上限没用),或模型本身太长(本部分新测:每个
fetch_add占两个分支,约 495 次撞上,调大上限即可)。 - panic in a destructor during cleanup,测试进程 SIGABRT:死锁后栈上有 loom
Arc(E10),或 spawn 之后第一次切换之前出错、排队的闭包里有 loomArc(本部分新测);#[should_panic]的死锁测试因此失败。 - Reached unexpected exploration state. Is the model fully deterministic?:重放走偏;
HashMap迭代顺序这类不确定来源还可能表现成 loom 内部的「assertion `left != right` failed」(本部分新测)。 - assertion failed: self.threads.len() < self.max():含主线程最多 5 个线程(E12)。
设置没有生效
- 设了 LOOM_MAX_PERMUTATIONS / LOOM_MAX_DURATION,模型照样跑满:只在每 20 000 轮检查一次(E4)。
- 设了 LOOM_CHECKPOINT_FILE,失败后文件却不存在;或者测试只跑了 1 轮就通过:只在整 interval 轮写;残留文件让下次从中途开始(E3)。
结果和真实语义不一致
- 用 SeqCst 写的正确算法,loom 报断言失败:SeqCst 访问按 AcqRel 处理,假阳性(E5)。
- Condvar 用 if 等待,loom 也通过了:不虚假唤醒,假阴性;
wait_timeout不超时,假阳性(E9)。 - invalid internal loom state:同线程嵌套 read() 之后 abort:读者集合按线程不按次数,假阳性;没有写者优先,嵌套读的真实死锁也模拟不出(E14)。
flowchart TB state["执行状态只在 tick 期间存在"] --> p1["1 模型外 / 跨轮"] state --> p3["3 二次 panic 后 abort"] path["一轮的路径:重放 + 长度上限"] --> p2["2 分支上限"] path --> p4["4 不确定的模型"] fixed["定长数组 MAX_THREADS = 5"] --> p5["5 线程数"] loop["主循环每 interval 轮做一次杂事"] --> p6["6 上限不生效"] loop --> p7["7 checkpoint 文件"] mm["简化的语义"] --> p8["8 SeqCst 误报"] mm --> p9["9 Condvar 不虚假唤醒"] mm --> p10["10 RwLock 嵌套读"] p10 --> p3 p2 --> p3 p4 --> p3
按根源分:第 1、3 条都来自「loom 的执行状态只在调度器的 tick 期间存在」(协程调度器),第 3 条又是第 2、4、10 条的常见后续:它们的第一条 panic 发生在合适的时机时,进程会 abort。第 2、4 条来自 Path(Path:路径与深度优先),第 6、7 条来自 Builder::check 主循环里同一个 if(model 与 Builder)。第 8–10 条是 07 能力边界 里的三条限制在使用者那一侧的样子。按后果分:第 1–5 条有明确的报错;第 6、7 条静默,结果看起来是「通过」;第 8–10 条里第 8、10 条是误报,第 9 条是漏报,最难发现。
计划里删掉的两条
卷计划列了 12 条候选,其中 not-deterministic、lazy-static-twice、child-panic 标为「先测,不成立或太小众就删」。
not-deterministic:保留(第 4 条)。复现成立,而且测出了计划没想到的一面:HashMap迭代顺序引起的走偏,12 次运行没有一次报「deterministic」那句,全是 loom 内部的assert_ne!。lazy-static-twice:删掉。 现象成立(E13:初始化函数里有调度点时,一轮初始化两次,析构 2 次),已经写在 线程、线程局部与 lazy_static。删掉的理由是触发条件窄:要初始化函数里有 loom 操作、两个线程都在做首次访问,并且初始化有外部可见的副作用,多出来的那份才会被注意到;被丢弃的那份会正常 drop,不报泄漏。本机 registry 里用loom::lazy_static!的只有 tokio(任务 id、线程 id 的计数器,初始化是AtomicU64::new(1)一类的构造)和 crossbeam-epoch(默认收集器Collector::new()),初始化都没有外部副作用。child-panic:删掉。 现象成立(E15:子线程 panic 直接让模型以那条消息失败,join拿不到Err),写在 线程、线程局部与 lazy_static 与 07 能力边界 的总表里。删掉的理由是用得到的模型少:要测的逻辑必须是「子线程 panic 之后,join的Err分支做了什么」。本机 registry 里有 loom 测试或切换层的 21 个 crate(按名字去重)中,唯一跟 panic 有关的 loom 测试是 tokio 的loom_atomic_waker,它就是在模型里自己catch_unwind,也就是那一页给出的绕法。
计划里的 permutations-not-honored、checkpoint-missing、deadlock-abort 三条按实测改了范围和 slug:第 6 条把时间上限一起写进去;第 7 条加上了「残留文件只跑 1 轮」这个更隐蔽的现象;第 3 条实测发现 abort 不限于死锁,改名 abort-after-panic。
本部分的实验
exp-loom/pitfalls/tests/pit.rs,./run_each.sh pit 逐个在独立进程里运行并打印退出码:
| 用例 | 看什么 | 页 |
|---|---|---|
outside_setup、outside_setup_fixed、object_across_rounds | model 外构造;挪进闭包;跨轮保存 | 1 |
long_400 … long_1000_raised | 模型长度与分支上限 | 2 |
should_panic_abba_loom_arc、should_panic_abba_leaked、assert_after_* | #[should_panic] 的死锁测试;spawn 后第一次切换前出错 | 3 |
nondet_hashmap、nondet_btreemap、nondet_std_once_flag | 三种不确定来源与反向验证 | 4 |
threads_4、threads_5 | E12 重跑 | 5 |
其余各页的数字引用前面各章的实验:E3、E4、E5、E9、E10、E11、E14。