持锁时 panic,下一个线程照常拿到锁和改了一半的数据
主线位置:主线 1 的出口。守卫析构 →
raw.unlock(),中间没有任何检查(RawMutex 的一字节状态机)。unwind 会把锁放开,数据停在 panic 那一刻的样子。
sequenceDiagram participant W as 线程 W participant M as Mutex(a=50, b=50) participant R as 线程 R W->>M: lock() W->>M: a -= 10 W->>W: panic!(b += 10 没执行) W->>M: unwind:守卫 drop,unlock() R->>M: lock():parking_lot 照常返回 R->>R: 读到 a=40, b=50,不变量 a+b=100 已破坏 Note over R: std 在这里返回 Err(PoisonError)
现象
一个后台线程 panic 了(日志里可能有,也可能被吞掉),程序继续跑,过一段时间出现「不可能」的数据:计数对不上、索引指向不存在的元素。没有任何地方报告锁曾经在 panic 中被释放。
最小复现
exp-parking_lot/pitfalls,poison-pl 与 poison-std 两个用例,各跑 3 次。锁里是两个账户,不变量 a + b == 100;工作线程在两次写之间 panic:
fn transfer_then_panic(acc: &mut Account) {
acc.a -= 10;
if acc.a < 1000 {
panic!("simulated failure between the two writes");
}
acc.b += 10;
}
输出(3 次相同):
poison-pl: worker panicked = true
poison-pl: lock() ok, a = 40, b = 50, a + b = 90
poison-std: worker panicked = true
poison-std: lock() Err: poisoned lock: another task failed inside
poison-std: into_inner a = 40, b = 50, a + b = 90
数据本身两边一样都坏了;区别只在 std 用 Err 告诉了下一个持锁者,parking_lot 什么也不说。
源码出处
- parking_lot 的文档把它列为与 std 的差异之一:「No poisoning, the lock is released normally on panic」(
src/mutex.rs:41,RwLock同样,src/rwlock.rs:54)。 MutexGuard::drop只有一行self.mutex.raw.unlock()(lock_api/src/mutex.rs:701-708)。- std 的守卫多一个
poison::Guard,记录加锁时线程是否已在 panic,drop 时据此置毒化标志(和 std Mutex 比);报错文字来自sync/poison.rs:252。
parking_lot 里唯一会毒化的是 Once,见 Once 毒化之后。
怎么改
从 std 迁移时,m.lock().unwrap() 变成 m.lock(),顺手去掉的其实是「上一个持锁者 panic 了」这条信号。要不要补回来,看数据有没有跨多次写的不变量:
- 让修改要么全做、要么不做:先在锁外算出新值,锁里一次赋值(
*g = new_state;),或者先改副本再mem::swap。panic 只可能发生在赋值之前,锁里的数据始终是完整的。 - 需要知道「出过事」:自己在数据里放一个标志,进入多步修改前置位、完成后清零;或者用
scopeguard::defer!/ 一个自定义Drop在std::thread::panicking()时记下。 - panic 本来就该结束进程:
panic = "abort"时没有 unwind,也就不存在「下一个线程看到半更新数据」的问题。 - 反过来,std 的毒化在很多代码里被
unwrap()变成连锁 panic,或用into_inner()忽略;parking_lot 省掉的是这层样板。两者没有绝对优劣,关键是知道自己去掉了什么。
小结
- parking_lot 的
Mutex/RwLock没有毒化,守卫 drop 只解锁。 - 实测:两次写之间 panic,parking_lot 下一次
lock()正常返回,a + b为 90;std 返回PoisonError,into_inner里是同样坏掉的数据。 - 迁移时去掉
unwrap也去掉了 panic 信号;有跨多次写的不变量,就让修改原子化或自己记标志。 - parking_lot 里只有
Once会毒化。
💬 关联讨论
发起讨论加载中...