死锁主因是mutex加锁顺序不一致,应统一用std::scoped_lock;条件变量必须配std::unique_lock;共享数据须用std::atomic或锁保护;RAII锁需严格作用域控制防异常泄漏。

死锁的典型现场:std::mutex嵌套加锁就卡住
线程 A 持有 mtx_a 同时想拿 mtx_b,线程 B 持有 mtx_b 同时想拿 mtx_a——程序不动了,gdb 里看两个线程都停在 pthread_mutex_lock。这不是偶发,是加锁顺序不一致的必然结果。
最直接的解法是统一加锁顺序:所有地方都先锁地址小的 mutex,再锁大的。但手写比较逻辑容易漏、难维护。C++17 起推荐用 std::scoped_lock:
std::mutex mtx_a, mtx_b; // 安全:自动按地址排序,无死锁风险 std::scoped_lock lock(mtx_a, mtx_b);
-
std::scoped_lock是std::lock_guard的多互斥量升级版,构造时原子性获取全部锁 - 别用
std::lock_guard套多个实例,它不保证顺序,反而可能引入竞态 - C++14 及以前只能手动调用
std::lock(mtx_a, mtx_b),再配std::adopt_lock,步骤多、易错
条件变量没配 std::unique_lock 就等不到唤醒
写 cv.wait() 时传了 std::lock_guard,编译过不了——std::condition_variable::wait 要求锁类型必须支持转移(move),而 std::lock_guard 不可拷贝也不可移动。
正确姿势只有一条:std::condition_variable 必须搭配 std::unique_lock:
立即学习“C++免费学习笔记(深入)”;
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
// ✅ 正确
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, []{ return !data_queue.empty(); });
-
std::unique_lock允许临时释放锁(lk.unlock()),这是wait内部实现等待+唤醒的关键 - 别图省事用
std::lock_guard包一层再传进去,类型不匹配,编译失败 - 如果只是简单保护临界区且不需要条件等待,
std::lock_guard更轻量;一旦涉及wait或需手动控制锁生命周期,必须切到std::unique_lock
共享数据没加 std::atomic 或锁,优化后行为诡异
两个线程读写同一个 bool ready 标志位,一个设为 true,另一个轮询等待。结果有时永远等不到——不是逻辑错,是编译器重排 + CPU 缓存不一致。
基础规则:所有跨线程读写的非 const 变量,要么用 std::atomic,要么包在锁里。别信“这只是个 bool,应该没问题”:
- 用
std::atomic_bool ready{false};最简洁,读写自动带内存序,默认memory_order_seq_cst - 若需更高性能(如无锁队列),才考虑放宽内存序,但必须成对设计,
store和load序号要匹配 - 千万别用
volatile替代std::atomic,它不提供线程同步语义,现代编译器下基本无效
RAII 失效:异常抛出时锁没释放
锁住 mtx 后,中间调用某个函数抛了异常,std::lock_guard 析构被跳过,mtx 永久卡死——这种情况在真实项目里比想象中常见,尤其当封装逻辑变深、异常路径增多时。
根本原因是用了裸 mtx.lock()/mtx.unlock(),或 RAII 对象作用域被意外截断。确保锁一定被释放,只靠一条原则:
- 锁对象声明必须紧贴临界区起始,且不能放在
if分支、循环体或函数返回前的局部作用域里 - 避免把
std::lock_guard声明在try块外、然后在catch里手动unlock——这等于放弃 RAII - 更稳妥的做法是把临界区逻辑抽成独立函数,让锁对象生命周期清晰可控
并发安全不是加了锁就万事大吉,是每处共享访问都要对齐内存模型、每把锁的生命周期都要落在异常安全的路径上。漏掉任意一环,问题可能半年都不复现一次,但线上就是卡死。









