死锁就是两个或多个进程/线程互相等待对方释放资源,谁都走不下去,永久卡住。
四个必要条件(Coffman 条件)
缺一不可,同时满足才会死锁:
互斥(Mutual Exclusion)
资源同一时刻只能被一个进程占用占有并等待(Hold and Wait)
进程已经拿着至少一个资源,还在等其他资源不可抢占(No Preemption)
资源不能被强制剥夺,只能由持有者主动释放循环等待(Circular Wait)
进程之间形成环形等待链:A 等 B,B 等 C,C 等 A
死锁检测
资源分配图(RAG)
- 进程 → 资源:请求边
- 资源 → 进程:分配边
- 图中有环 = 可能死锁(单实例资源有环必死锁)
银行家算法(检测变体)
维护 Available / Allocation / Need 矩阵,模拟能否找到一个安全序列让所有进程完成。找不到 → 死锁。
实际系统做法
- 定期运行检测算法
- 发现死锁后:杀掉代价最小的进程,或回滚事务(数据库常用)
死锁预防
破坏四个条件之一即可:
| 条件 | 破坏方式 | 代价 |
|---|---|---|
| 互斥 | 用无锁结构(CAS/原子操作) | 不是所有资源都能无锁化 |
| 占有并等待 | 一次性申请所有资源,或申请前释放已有资源 | 资源利用率低 |
| 不可抢占 | 申请失败时强制释放已有资源 | 实现复杂,可能导致饥饿 |
| 循环等待 | 给资源编号,强制按序申请(最常用) | 需要全局规划资源顺序 |
最实用的方案:资源有序申请
给所有锁/资源编号,所有线程必须按编号从小到大申请,永远不会形成环。
# 错误示范:可能死锁
# 线程1: lock_a -> lock_b
# 线程2: lock_b -> lock_a
# 正确做法:统一按编号顺序申请
# lock_a 编号 1,lock_b 编号 2
# 所有线程都先拿 lock_a,再拿 lock_b
with lock_a:
with lock_b:
# 临界区
pass
死锁避免(顺带一提)
预防是静态限制,避免是动态决策——每次分配前用银行家算法判断是否会进入不安全状态,不安全就拒绝分配。理论上完美,实际上因为需要提前知道最大需求量,工程中很少用。
总结
- 预防:破坏四个条件之一,最常用是资源有序申请
- 检测:资源分配图或银行家算法,发现死锁后杀进程或回滚
- 避免:银行家算法动态判断,理论完美但工程少用