Rust in Everything
基于 loom 0.7.2

常见坑

每一页对着一个报错或一个现象:先给最小复现和实测输出,再到源码里找它从哪来,最后给出改法。机制在前面各章讲过的只链过去,能复用前面实验的直接引用数字。本部分新做的复现在 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。交错数是确定的。

报错原文

  1. cannot access Loom execution state from outside a Loom model:在 model 外构造或使用 loom 类型;把 loom 对象留到下一轮则报「failed to get object for ref」(本部分新测)。
  2. Model exceeded maximum number of branches:不 yield 的自旋(E11,调大上限没用),或模型本身太长(本部分新测:每个 fetch_add 占两个分支,约 495 次撞上,调大上限即可)。
  3. panic in a destructor during cleanup,测试进程 SIGABRT:死锁后栈上有 loom Arc(E10),或 spawn 之后第一次切换之前出错、排队的闭包里有 loom Arc(本部分新测);#[should_panic] 的死锁测试因此失败。
  4. Reached unexpected exploration state. Is the model fully deterministic?:重放走偏;HashMap 迭代顺序这类不确定来源还可能表现成 loom 内部的「assertion `left != right` failed」(本部分新测)。
  5. assertion failed: self.threads.len() < self.max():含主线程最多 5 个线程(E12)。

设置没有生效

  1. 设了 LOOM_MAX_PERMUTATIONS / LOOM_MAX_DURATION,模型照样跑满:只在每 20 000 轮检查一次(E4)。
  2. 设了 LOOM_CHECKPOINT_FILE,失败后文件却不存在;或者测试只跑了 1 轮就通过:只在整 interval 轮写;残留文件让下次从中途开始(E3)。

结果和真实语义不一致

  1. 用 SeqCst 写的正确算法,loom 报断言失败:SeqCst 访问按 AcqRel 处理,假阳性(E5)。
  2. Condvar 用 if 等待,loom 也通过了:不虚假唤醒,假阴性;wait_timeout 不超时,假阳性(E9)。
  3. 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_roundsmodel 外构造;挪进闭包;跨轮保存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_5E12 重跑5

其余各页的数字引用前面各章的实验:E3、E4、E5、E9、E10、E11、E14。

💬 关联讨论

发起讨论
加载中...