Apollo 技术文档Apollo 技术文档
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
  • MMORPG 架构 QA

Q72: 如何避免死锁?

核心结论

避免死锁最有效的办法,不是事后检测,而是从一开始就减少多锁共享和不一致的资源获取顺序。

更务实的原则通常是:

  • 能不共享就不共享
  • 能单线程归属就不要多线程抢
  • 必须加锁时统一顺序
  • 把持锁时间压到最短

死锁本质上是系统边界和资源获取顺序设计问题,不只是代码技巧问题。

一、死锁真正是怎么来的

最常见的死锁场景通常不是复杂算法,而是:

  • 线程 A 拿了锁 1,等锁 2
  • 线程 B 拿了锁 2,等锁 1

一旦资源获取顺序不一致,死锁就很容易出现。

所以比记四个必要条件更重要的是:先找到系统里哪些操作可能同时拿多把锁。

二、最有效的预防手段通常是减少共享

如果对象天然可以归属于:

  • 某个线程
  • 某个 Actor
  • 某个逻辑分区

那就尽量避免多个线程同时修改它。

这是比“设计复杂锁协议”更稳的做法。

三、必须加多把锁时,顺序一定要统一

这是最经典也最实用的规则之一。

例如:

  • 永远按对象 ID 小到大加锁
  • 永远先拿账户锁,再拿订单锁

只要顺序统一,循环等待概率会大幅下降。

四、持锁范围要尽量小

很多死锁不是锁数量多,而是:

  • 锁拿得太久
  • 持锁期间做 I/O
  • 持锁期间调用外部模块

这会把死锁窗口明显放大。

更稳妥的做法通常是:

  • 先收集必要数据
  • 再持锁做最小修改
  • 释放锁后做后续逻辑

五、避免在锁内调用不可控逻辑

例如:

  • 回调
  • RPC
  • 数据库访问
  • 复杂脚本执行

这些调用链你通常无法保证内部不会再拿别的锁,最容易制造隐蔽死锁。

六、检测和超时是补充,不是主方案

死锁检测、锁超时和 watchdog 都很有价值,但它们更像止损手段。

真正的主方案仍然应该是:

  • 统一顺序
  • 减少共享
  • 缩小临界区

七、工程上更稳妥的设计习惯

常见习惯包括:

  • 为多锁操作规定固定顺序
  • 尽量避免锁嵌套
  • 公共设施层少暴露内部锁
  • 对热点锁做 tracing 和等待统计

这样问题能在变成线上事故前被发现。

八、常见误区

1. 只要用 std::scoped_lock 就不会死锁

它能帮一部分场景,但不能替代系统级资源顺序设计。

2. 死锁只会出现在复杂代码里

现实里很多死锁来自很普通的双对象操作。

3. 发现死锁后加重试就行

重试能缓解部分问题,但不能替代根因修复。

参考资料

  • 并发死锁预防、锁顺序和 tracing 实践资料
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu