Q72: 如何避免死锁?
核心结论
避免死锁最有效的办法,不是事后检测,而是从一开始就减少多锁共享和不一致的资源获取顺序。
更务实的原则通常是:
- 能不共享就不共享
- 能单线程归属就不要多线程抢
- 必须加锁时统一顺序
- 把持锁时间压到最短
死锁本质上是系统边界和资源获取顺序设计问题,不只是代码技巧问题。
一、死锁真正是怎么来的
最常见的死锁场景通常不是复杂算法,而是:
- 线程 A 拿了锁 1,等锁 2
- 线程 B 拿了锁 2,等锁 1
一旦资源获取顺序不一致,死锁就很容易出现。
所以比记四个必要条件更重要的是:先找到系统里哪些操作可能同时拿多把锁。
二、最有效的预防手段通常是减少共享
如果对象天然可以归属于:
- 某个线程
- 某个 Actor
- 某个逻辑分区
那就尽量避免多个线程同时修改它。
这是比“设计复杂锁协议”更稳的做法。
三、必须加多把锁时,顺序一定要统一
这是最经典也最实用的规则之一。
例如:
- 永远按对象 ID 小到大加锁
- 永远先拿账户锁,再拿订单锁
只要顺序统一,循环等待概率会大幅下降。
四、持锁范围要尽量小
很多死锁不是锁数量多,而是:
- 锁拿得太久
- 持锁期间做 I/O
- 持锁期间调用外部模块
这会把死锁窗口明显放大。
更稳妥的做法通常是:
- 先收集必要数据
- 再持锁做最小修改
- 释放锁后做后续逻辑
五、避免在锁内调用不可控逻辑
例如:
- 回调
- RPC
- 数据库访问
- 复杂脚本执行
这些调用链你通常无法保证内部不会再拿别的锁,最容易制造隐蔽死锁。
六、检测和超时是补充,不是主方案
死锁检测、锁超时和 watchdog 都很有价值,但它们更像止损手段。
真正的主方案仍然应该是:
- 统一顺序
- 减少共享
- 缩小临界区
七、工程上更稳妥的设计习惯
常见习惯包括:
- 为多锁操作规定固定顺序
- 尽量避免锁嵌套
- 公共设施层少暴露内部锁
- 对热点锁做 tracing 和等待统计
这样问题能在变成线上事故前被发现。
八、常见误区
1. 只要用 std::scoped_lock 就不会死锁
它能帮一部分场景,但不能替代系统级资源顺序设计。
2. 死锁只会出现在复杂代码里
现实里很多死锁来自很普通的双对象操作。
3. 发现死锁后加重试就行
重试能缓解部分问题,但不能替代根因修复。
参考资料
- 并发死锁预防、锁顺序和 tracing 实践资料
