Rust in Everything
基于 loom 0.7.2

invalid internal loom state:同线程嵌套 read() 之后 abort

主线位置:主线 4 在 RwLock 上的两条:同线程嵌套读报内部错误(假阳性),没有写者优先(假阴性)。机制见 Mutex 与 RwLock。

flowchart TB
  code["同一线程:read() 还没放,又 read()"] --> loom["loom:读者集合是线程 id 的 HashSet,按线程不按次数"]
  loom --> e["内层守卫 drop 后集合已空;外层 drop 时 invalid internal loom state → 二次 panic → abort"]
  code --> real["真实实现:有写者排队、写者优先时死锁(parking_lot、本机 std)"]
  real --> miss["loom 没有写者优先,这种死锁本来也报不出"]

现象

invalid internal loom state
cannot access Loom execution state from outside a Loom model. …
thread caused non-unwinding panic. aborting.

第一行来自 rt/rwlock.rs:105,后面两行是展开中的二次 panic(panic 之后进程 abort)。

最小复现

E14(Mutex 与 RwLock):主线程 read() 里再 read(),另一个线程 write()。

写法结果
嵌套:第一个读守卫还活着时再 read()第 1 个交错「invalid internal loom state」,随后 abort
不嵌套:两个先后独立的 read() 块(反向验证)9 个交错,通过

源码出处

读者集合是 HashSet<thread::Id>(rt/rwlock.rs:12-16)。同一线程第二次 read 只是重复插入,集合里还是一个元素。内层守卫 drop 时 release_read_lock 把这个线程删掉,集合空了,锁被置成 None;外层守卫 drop 时再进 release_read_lock,匹配不到 Locked::Read,落到 _ => panic!("invalid internal loom state")(105 行)。也就是说,loom 的读者集合按线程记,不按次数记,「同一线程持有两个读守卫」在这个表示里根本写不下。std 的 RwLock 文档只说这样做可能死锁或 panic,并不禁止。

另一面:loom 的读者只要没有写者持锁就能进,写者在等时新读者照样进(Mutex 与 RwLock 实测 9 个交错里 3 个)。真实世界里嵌套读的危险正好来自写者优先:parking_lot 与本机 std 在「同线程读两次、中间有写者排队」时都 3/3 卡死(parking_lot 卷 recursive-read)。loom 没有写者优先,就算不报内部错误,也模拟不出这个死锁。

怎么改

  • 被测代码:不要在持有读守卫时调用可能再读同一把锁的函数,先把要的数据复制出来、放掉守卫。这本来就是真实环境里的正确做法。
  • 模型:确实需要嵌套读的路径,loom 测不了;在模型里改成先后两次独立的读,嵌套读的死锁风险靠代码审查和 parking_lot 那种压力复现。
  • 报错之后会 abort,这类测试分进程跑。

小结

  • 同线程嵌套读:loom 第 1 个交错报「invalid internal loom state」(rwlock.rs:105)后 abort,这是假阳性。
  • loom 的 RwLock 没有写者优先,嵌套读在真实实现上的死锁它也模拟不出,这是假阴性。
  • E14 反向:改成不嵌套的两次读,9 个交错通过。
  • 改法:读守卫不跨可能重入的调用;模型里不写嵌套读。

💬 关联讨论

发起讨论
加载中...