Deadly embrace即死锁,需通过gdb观察线程阻塞在mutex或condition_variable上且存在循环等待来确认;90%源于多线程锁获取顺序不一致,须强制统一顺序并避免锁内调用、析构加锁等危险模式。

Deadly embrace就是死锁,先确认是不是真死锁
看到程序卡住、CPU降为0、线程长时间无响应,别急着断定是死锁——先用 gdb 附加进程看线程状态:gdb -p <pid>,然后执行 info threads 和对每个线程跑 bt。如果多个线程都停在 pthread_mutex_lock、std::mutex::lock() 或 std::condition_variable::wait() 上,且各自持有某把锁又等待另一把,那基本就是死锁。
注意:std::timed_mutex::try_lock_for() 返回 false 不等于死锁,只是超时;而 std::mutex 永远不会超时,一旦卡住,大概率就是死锁或误用了递归锁。
检查锁的获取顺序是否一致
90% 的 C++ 死锁源于不同线程以不同顺序获取同一组互斥量。比如线程 A 先 lock mtx_a 再 lock mtx_b,而线程 B 反过来先 lock mtx_b 再 lock mtx_a,就构成环路等待。
- 所有涉及多把锁的代码路径,必须约定全局一致的加锁顺序(例如按变量地址升序:
if (&mtx_a < &mtx_b) { mtx_a.lock(); mtx_b.lock(); } else { mtx_b.lock(); mtx_a.lock(); }) - 避免在已持有一把锁时,调用可能间接申请另一把锁的函数(比如某个
update_cache()内部悄悄用了global_config_mutex) - 慎用
std::lock_guard嵌套作用域——它不解决顺序问题,反而容易掩盖调用链中的隐式锁依赖
用静态/动态工具辅助定位
靠肉眼和日志很难覆盖所有并发路径,得借助工具:
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
立即学习“C++免费学习笔记(深入)”;
- 编译时加
-D_GLIBCXX_DEBUG(GCC)可让std::mutex在重复 lock 同一线程时报错,但不检测跨线程死锁 - 运行时用
helgrind(valgrind 子工具):启动命令为valgrind --tool=helgrind ./your_program,它会报告“possible data race”和“lock order inconsistency”,重点看后者 - Linux 下可用
perf record -e sched:sched_lock_wait结合perf script观察锁等待热点,但需内核开启相应 tracepoint - Clang + ThreadSanitizer(
-fsanitize=thread)能捕获部分死锁前兆(如循环等待日志),但对纯 mutex 死锁检出率有限,更擅长数据竞争
避免常见低级错误模式
有些写法看着合理,实则埋雷:
-
std::recursive_mutex不是救命稻草——它只防同一线程重入,完全不管跨线程顺序,滥用反而让死锁更难复现 - 在析构函数里调用需要加锁的函数(比如
~Connection() { send_shutdown_packet(); }),而此时对象可能正被其他线程通过 shared_ptr 访问,极易形成锁依赖反转 - 把
std::condition_variable::wait()当成普通等待用,却忘了它在唤醒后要重新检查谓词——若没配好 while 循环 + 锁保护,可能跳过通知并永久阻塞,现象类似死锁 - 使用
std::shared_mutex时混用lock_shared()和lock(),尤其在读多写少场景下,一个写线程等所有读锁释放,而新读线程又在等写线程释放锁,形成饥饿型伪死锁
真正棘手的死锁往往藏在异常路径里:比如 try-catch 块中提前 return 却没 unlock,或者智能指针析构触发回调进而申请锁。这类问题必须结合 RAII 和严格的作用域设计来规避,光靠工具扫不出来。

















