死锁日志必须记录线程id、锁地址、操作类型、时间戳和全局序号。需用无锁队列写入,避免引入竞争;推荐raii封装trackedmutex并覆盖condition_variable::wait;默认只记lock_blocked/acquired,支持全量模式与请求id关联;时间戳必须用steady_clock。

死锁日志必须记录线程 ID 和锁操作序列
只打印“获取锁失败”或“等待 mutex”毫无价值。关键是要在每次 std::mutex::lock()、std::mutex::try_lock()、std::mutex::unlock() 前后,用原子方式打点:线程 ID(std::this_thread::get_id())、锁对象地址(&m)、操作类型、时间戳(高精度,如 std::chrono::steady_clock::now())。否则你根本分不清是哪个线程在等哪把锁。
常见错误是用 std::cout 或未同步的全局缓冲区写日志——这本身就会引入竞争,甚至掩盖或诱发死锁。必须用无锁队列(如 moodycamel::ConcurrentQueue)或静态 std::atomic_flag 保护的环形缓冲区,确保日志写入不依赖任何可能被卡住的锁。
日志格式要支持因果推断,不能只记“谁在等”
单纯记录 thread A waiting for mutex 0x7fff12345678 不够。你需要能回溯“谁持有它”以及“它之前还等过什么”。因此每条日志建议包含:
-
tid(线程 ID 字符串化,避免std::thread::id的流输出开销) -
op("lock_enter"/"lock_blocked"/"lock_acquired"/"unlock") -
mutex_addr(十六进制,统一宽度,如0x00007fff12345678) -
backtrace(可选但极有用:在lock_blocked时捕获栈帧,用backtrace()+dladdr()) -
seq(全局单调递增序号,用于排序,避免依赖系统时间精度)
这样当死锁发生后,你 grep 出所有 lock_blocked 日志,按 seq 排序,就能看到阻塞链:A 等 B,B 等 C,C 等 A。
别依赖运行时自动检测,得主动埋点 + 外部分析
C++ 标准库不提供死锁检测钩子,std::mutex 也不暴露内部状态。指望 gdb attach 后看 info threads 和 bt 只能告诉你“现在卡在哪”,无法还原“怎么卡成这样”。必须前置埋点。
实操建议:
- 用 RAII 封装锁:写一个
TrackedMutex,在构造函数(lock)和析构函数(unlock)里打日志;std::unique_lock<trackedmutex></trackedmutex>自动覆盖大部分场景 - 对
std::condition_variable::wait()也做类似封装,因为它内部会 unlock + re-lock,容易成为隐式等待点 - 死锁发生后,立刻
kill -SIGUSR1 <pid></pid>触发日志 dump 到文件(避免最后几条日志还在内存中) - 用 Python 脚本解析日志:按
mutex_addr分组找持有者,再按tid追路径,生成等待图(waiting graph)
生产环境日志量大,得过滤和采样
全量记录每把锁的每一次操作,在高并发下 I/O 和内存压力巨大。实际做法是分级:
- 默认只记录
lock_blocked和lock_acquired(即真正可能出问题的点) - 通过环境变量(如
LOG_LOCK_TRACE=1)开启全量模式,仅用于复现阶段 - 对高频锁(如全局计数器锁)加白名单过滤,避免淹没关键路径
- 日志行末加
/* trace_id: xxx */,方便和业务请求 ID 关联,定位具体哪个请求触发了死锁链
最容易被忽略的是:日志时间戳必须用 steady_clock,不能用 system_clock —— NTP 调整会让时间跳变,导致你误判“A 在 B 之后申请锁”,而实际是时钟倒退了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











