std::lock_guard异常处理中的死锁问题及最佳实践咨询
一、std::lock_guard 的正确使用姿势
严格统一锁的获取顺序
这是规避死锁的核心规则。如果多个线程需要获取多把互斥锁,必须保证所有线程都按照完全一致的顺序获取锁。绝对不能出现线程A先锁mutex1再锁mutex2,线程B先锁mutex2再锁mutex1的交叉情况。
示例:std::mutex m1, m2; void thread_func1() { std::lock_guard<std::mutex> lock1(m1); // 仅在必要时持有锁,避免耗时操作 std::lock_guard<std::mutex> lock2(m2); // 操作共享资源 } void thread_func2() { std::lock_guard<std::mutex> lock1(m1); // 和thread_func1保持完全一致的锁顺序 std::lock_guard<std::mutex> lock2(m2); // 操作共享资源 }用 std::lock 批量获取多把锁
如果必须同时持有多把锁,不要逐个用lock_guard获取,而是用std::lock一次性锁定所有互斥量,再通过lock_guard配合std::adopt_lock标记接管锁的所有权——std::lock内部会处理锁的顺序,避免交叉获取导致死锁。
示例:std::mutex m1, m2; void safe_multi_lock_func() { std::lock(m1, m2); // 一次性锁定两个互斥量,内部规避死锁风险 std::lock_guard<std::mutex> lock1(m1, std::adopt_lock); std::lock_guard<std::mutex> lock2(m2, std::adopt_lock); // 操作共享资源 }避免同一线程重复获取不可重入锁
你遇到的问题大概率是同一线程嵌套尝试获取std::mutex(默认不可重入):lock_guard构造时会调用lock(),而不可重入互斥量在同一线程重复lock会直接阻塞当前线程,造成死锁。
错误示例:std::mutex m; void throw_an_exception() { std::lock_guard<std::mutex> lock(m); throw std::runtime_error("test error"); } void another_func() { std::lock_guard<std::mutex> lock(m); throw_an_exception(); // 已持有m的线程再次尝试获取m,阻塞死锁 }解决方法:优先重构代码,避免同一线程重复持有同一锁;如果业务必须重入,改用
std::recursive_mutex(但不推荐,容易掩盖代码设计问题)。最小化锁的持有时间
锁持有时间越长,死锁概率越高。仅在操作共享资源的代码块持有锁,不要在锁内执行耗时操作(比如IO、睡眠、调用未知外部函数)。
二、常见死锁示例及规避方法
1. 交叉锁顺序死锁
std::mutex m1, m2; void thread1() { std::lock_guard<std::mutex> l1(m1); std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作 std::lock_guard<std::mutex> l2(m2); // 等待thread2释放m2 } void thread2() { std::lock_guard<std::mutex> l2(m2); std::this_thread::sleep_for(std::chrono::seconds(1)); std::lock_guard<std::mutex> l1(m1); // 等待thread1释放m1,互相等待死锁 }
规避:所有线程严格遵循同一锁获取顺序,比如先锁m1再锁m2。
2. 递归调用导致重复锁死锁
std::mutex m; void recursive_func() { std::lock_guard<std::mutex> lock(m); // 逻辑错误:递归调用自身,再次尝试获取同一锁 recursive_func(); // 线程阻塞,死锁 }
规避:重构代码取消递归持有同一锁;特殊场景下改用std::recursive_mutex。
3. 手动锁未在异常时释放(非lock_guard场景,但需警惕)
std::mutex m; void bad_manual_lock_func() { m.lock(); if (some_error_condition) { throw std::runtime_error("error"); // 未解锁,锁永久持有,其他线程等待死锁 } m.unlock(); }
规避:永远用lock_guard或unique_lock这类RAII工具管理锁,禁止手动调用lock/unlock。
4. 持有锁时调用未知外部函数
std::mutex m; void external_func(); // 外部函数,内部可能获取其他锁 void risky_func() { std::lock_guard<std::mutex> lock(m); external_func(); // 如果external_func获取了m2,而另一个线程持有m2并等待m,触发死锁 }
规避:要么确保外部函数不会获取其他锁,要么在调用外部函数前释放当前锁,要么重新设计锁的粒度拆分资源。
内容的提问来源于stack exchange,提问作者Michael

