外观
第 1 节:死锁——两只蚂蚁互相等
症状
程序编译通过、运行正常,然后……卡死。没有报错、没有崩溃,就是不动了。CPU 占用可能还是 0(两个线程都在干等)。
诊断
死锁(deadlock):线程 A 拿着锁 1 等锁 2,线程 B 拿着锁 2 等锁 1——谁都不放手,谁都在等,永远等不到。
实测(两个线程各拿一把锁,然后互相等对方的那把):
运行输出
text
蚂蚁 1 拿到了锁 A,等锁 B……
蚂蚁 2 拿到了锁 B,等锁 A……
(永远卡在这里)两只蚂蚁的故事(第 15 章):蚂蚁 1 抢到糖罐 A,蚂蚁 2 抢到糖罐 B,然后蚂蚁 1 想要 B、蚂蚁 2 想要 A——互相等,永远不动。
死锁的条件(四个都满足才会死):
- 互斥:锁一次只能一个人用(第 15 章 Mutex 本性)
- 持有并等待:拿着 A 不放手,还等 B
- 不可剥夺:谁也不能抢走别人手里的锁
- 循环等待:A 等 B,B 等 A
解药
解药一:锁的顺序统一(最实用)——所有线程都"先拿 A 再拿 B",永不交叉:
rust
// 死锁版:蚂蚁 1 拿 A 等 B,蚂蚁 2 拿 B 等 A
// 解药:两只蚂蚁都"先 A 后 B"——拿不到 B 就松手 A,绝不死等
fn lock_pair(a: &Mutex<()>, b: &Mutex<()>) {
let _a = a.lock().unwrap();
let _b = b.lock().unwrap(); // 顺序永远一致
}解药二:锁里别等锁(锁的粒度要小)——需要两把锁的活,拆开干:
rust
// 慢而险:一把锁里再拿另一把
let _a = a.lock().unwrap();
let _b = b.lock().unwrap(); // 这里可能死等
// 好:各自锁各自的,锁外合并结果
let result_a = read_from(&a);
let result_b = read_from(&b);解药三:抢锁超时——等不到就算了,别死等(Rust 标准库 Mutex 没有超时;用 try_lock 拿不到就放弃/重试):
rust
if let Ok(_a) = a.try_lock() {
if let Ok(_b) = b.try_lock() {
// 都拿到了,干活
}
// 拿不到 B?松手 A,下次再试——绝不抱着 A 死等 B
}预防
- 一把锁 = 安全区:多把锁的程序才可能死锁——能一把锁搞定的,别拆两把(性能篇第 3 节"锁要小"和这里的平衡:锁要小,但不能小到"锁里再锁")
- 多把锁的铁律:全局统一加锁顺序(资源编号排序,A 永远在 B 前拿)
- Rust 能防数据竞争,防不了死锁(第 15 章原话)——死锁是逻辑问题,只能靠设计纪律
- 怀疑死锁时:看两个线程的堆栈(
RUST_BACKTRACE或调试器),停在lock()上的就是嫌疑人