死锁必然发生于两线程循环等待对方持有的锁;C++无运行时检测,std::mutex卡住即永久挂起;须按固定顺序加锁,优先用std::scoped_lock防顺序错误,但不解决粒度与跨函数问题。

死锁不是“会不会发生”,是“什么时候触发”
只要两个线程各自持有对方需要的锁、又同时申请对方的锁,死锁就必然发生。C++ 里没有运行时检测机制,std::mutex 一旦卡住,程序就永久挂起——连超时重试都做不到。
常见错误现象:std::thread 阻塞在 lock() 或 std::scoped_lock 构造里,CPU 占用归零,调试器显示所有相关线程停在 __pthread_mutex_lock;用 gdb 查栈能看到多个线程互相等待不同 std::mutex 实例。
- 永远按**固定全局顺序**获取多个锁:比如所有函数都先锁
mutex_a再锁mutex_b,哪怕当前逻辑“看起来”只该先动 b - 用
std::scoped_lock替代手写多个lock()调用:它内部自动按地址排序加锁,避免顺序依赖出错 - 绝不混合使用
lock()/try_lock()和std::scoped_lock:前者可能部分成功后者全失败,逻辑断裂点极难追踪
std::scoped_lock 不是万能解药
std::scoped_lock 确实能防“同一批锁”的顺序死锁,但它不解决锁粒度、生命周期或跨函数调用链的问题。
使用场景:保护同一数据结构的多个成员变量,或需原子更新两个关联资源(如账户余额 + 交易日志)。
立即学习“C++免费学习笔记(深入)”;
参数差异:std::scoped_lock 构造时若任一锁不可得,会阻塞直到全部可得;而 std::try_lock 返回 -1 表示失败,但你得自己处理回滚逻辑。
- 别把它当“更安全的 lock_guard”:如果只传一个锁,
std::scoped_lock和std::lock_guard行为一致,但前者开销略大 - 不能跨作用域释放:锁在
scoped_lock对象析构时才释放,别试图提前unlock()—— 它没这个接口 - 和
std::unique_lock混用要小心:后者支持延迟加锁、条件变量、转移所有权,但手动管理多了,出错概率反而上升
std::shared_mutex 在读多写少时反而加重死锁风险
很多人以为“读锁不互斥”就能随便加,结果在混合读写场景下栽跟头:一个线程持 shared_lock,另一个线程持 unique_lock 等写锁,第三个线程又想拿 shared_lock —— 这时写锁线程被堵,读锁线程又等不到写锁释放,形成环路。
性能影响:Linux 下 std::shared_mutex 实现通常基于 futex,但公平性差;高并发读+偶发写时,新来的读请求可能饿死写线程。
- 只在明确满足“读操作远多于写操作 + 读操作耗时短”时才用,否则老实用
std::mutex - 禁止在持有
shared_lock期间调用可能升级为写的函数(比如某个 getter 内部悄悄触发 lazy init) - 不要嵌套:在已持
shared_lock的函数里再尝试拿unique_lock,等于主动制造死锁条件
std::condition_variable::wait 里的隐式锁释放是最大陷阱
wait() 会自动释放锁、挂起线程,唤醒后重新加锁——这一步看似省事,实则把锁的边界藏起来了。一旦 predicate 检查逻辑有误,或者唤醒信号丢失,线程就可能永远等下去。
错误现象:生产者调用了 notify_one(),消费者却没醒来;或者消费者醒来了,但数据还没准备好,又立刻回去等。
- 必须用 while 循环检查 predicate:
while (!ready) cv.wait(lock);,不用 if - notify 必须在修改共享状态后、且仍在同一把锁保护下发出,否则存在竞态窗口
- 别在 wait 前做耗时操作:比如先查数据库再等条件变量,会导致锁持有时间过长,拖慢其他线程
真正难的不是写对第一版多线程代码,而是改需求时——加个字段、换种通知方式、拆个类——这些动作会悄悄破坏原有的锁顺序或条件判断。每次改动后,用 helgrind 或 ThreadSanitizer 扫一遍,比靠人脑推演靠谱得多。










