gdb attach后用info threads和thread apply all bt查看所有线程堆栈,重点分析卡在pthread_mutex_lock、std::mutex::lock或__lll_lock_wait的线程,结合-O0 -g编译、TSan检测锁序反转、std::lock/scoped_lock避免分步加锁、mutex等级校验等手段综合预防和定位死锁。

gdb attach 后看所有线程堆栈,重点盯 pthread_mutex_lock 和 std::mutex::lock
死锁发生时程序卡住,gdb attach 是最快定位手段。别只查一个线程——先用 info threads 列出全部线程,再逐个执行 thread apply all bt 或手动 thread N + bt。真正有用的线索藏在停在 pthread_mutex_lock、std::mutex::lock 或 __lll_lock_wait 的帧里。
- 编译时加
-O0 -g,否则优化可能把 lock 调用内联掉,堆栈看不到你代码里的mu1.lock(),只看到底层符号 - 如果看到多个线程都卡在不同 mutex 的
lock()上,且各自已持有一个锁(往上翻几帧常能看到前一个lock_guard构造),基本就是顺序不一致导致的循环等待 - 注意
std::mutex的bt可能显示__pthread_mutex_lock,要往回翻 2–3 帧才能定位到你源码中哪一行调用了lock()
用 ThreadSanitizer 在运行前捕获锁顺序反转
ThreadSanitizer(TSan)不仅能报 data race,还能识别“锁顺序反转”(lock-order-inversion):只要同一线程在不同路径中以不同顺序获取了同一组锁,它就会在运行时告警。这不是事后分析,而是编译期埋点、运行期触发。
- 启用方式简单:
clang++ -fsanitize=thread -g -O2或g++ -fsanitize=thread -g -O2(GCC 9+) - 它不要求复现死锁,只要存在两种加锁路径(比如 A→B 和 B→A),TSan 就会报
lock-order-inversion并给出两个调用栈 - 注意:TSan 会显著拖慢运行速度(2–5 倍),只应在 debug build 或 CI 中启用;上线前务必关掉
用 std::lock 替代分步 lock_guard,避免中间状态
手写 mu1.lock(); mu2.lock(); 是高危操作——一旦第一个成功、第二个失败(或被中断),就留下不一致状态。而 std::lock 内部使用试探-回退算法,保证要么全部成功,要么全部失败,不会让线程卡在半锁状态。
- 正确用法:
std::lock(mu1, mu2); std::lock_guard<:mutex> lk1(mu1, std::adopt_lock); std::lock_guard<:mutex> lk2(mu2, std::adopt_lock);</:mutex></:mutex> - 不能直接对
std::lock_guard构造函数传两个 mutex——它只接受一个;必须配合std::adopt_lock标志告诉 RAII 对象“锁已由std::lock拿到” -
std::scoped_lock(C++17)是更简洁的替代:std::scoped_lock lk(mu1, mu2);,它内部自动调用std::lock,且支持任意数量 mutex
给 mutex 加等级编号,在 debug build 中做运行时顺序校验
静态约定(如“所有地方必须先锁 g_user_map_mu 再锁 g_session_list_mu”)容易被遗忘或违反。更可靠的做法是为每个 mutex 分配唯一整数等级,在每次 lock() 前检查当前线程已持有的锁是否都满足 level < this_level。
立即学习“C++免费学习笔记(深入)”;
- 等级必须全局唯一且稳定:类成员 mutex 的等级写在类注释里,全局变量直接命名带等级(如
g_user_map_mu_level_10) - 不要用地址比较(
&mu1 < &mu2)代替等级——地址在 ASLR 下不可靠,且跨进程/动态库时失效 - 可借助
ABSL_HAVE_THREAD_SANITIZER宏,在 TSan 开启时启用轻量级等级检查;未开启时跳过,不影响性能


















