std::mutex 必须配合 std::lock_guard 或 std::unique_lock 使用,手动调用 lock()/unlock() 易致死锁;应遵循 RAII 原则、统一多锁顺序、避免嵌套、禁用拷贝移动,并在性能敏感场景优先考虑原子操作或细粒度锁。

std::mutex 必须配合 std::lock_guard 或 std::unique_lock 使用
直接调用 mutex.lock() 和 mutex.unlock() 极易出错:忘记 unlock、异常中途退出、提前 return 都会导致死锁。C++ 标准库不鼓励手动配对,而是靠 RAII 自动管理生命周期。
正确做法是把 std::mutex 和 std::lock_guard(最常用)或 std::unique_lock(需要延迟加锁或转移所有权时用)一起声明在临界区作用域开头:
std::mutex mtx;
int shared_value = 0;
void increment() {
std::lock_guard lock(mtx); // 构造即加锁
++shared_value; // 访问共享资源
} // 析构自动 unlock —— 即使这里抛异常也安全
-
std::lock_guard不可复制、不可移动,构造后立即加锁,析构必解锁,开销最小 -
std::unique_lock更灵活(支持try_lock()、defer_lock、unlock()手动释放),但有轻微额外开销 - 绝不要把
lock_guard声明成全局或类成员变量——它必须和临界区生命周期严格对齐
多个 mutex 加锁顺序不一致会引发死锁
当一段逻辑需同时操作两个共享对象(比如转账:从 A 扣款 + 给 B 加款),若线程 1 先锁 A 再锁 B,而线程 2 先锁 B 再锁 A,就可能互相等待、永久阻塞。
解决方法不是“小心写”,而是强制统一顺序:
立即学习“C++免费学习笔记(深入)”;
- 按地址大小排序:
std::lock(mtx_a, mtx_b)是标准方案,它内部用无死锁算法批量加锁 - 或者约定固定顺序:比如所有代码都先锁
account_mutex[smaller_id],再锁account_mutex[larger_id] - 避免嵌套锁:一个函数里只锁一个 mutex;如需多资源,提取为独立函数并统一加锁顺序
错误示例(高风险):
// 线程1: mtx_a.lock(); mtx_b.lock(); // OK in thread 1 // 线程2: mtx_b.lock(); mtx_a.lock(); // 可能死锁
std::mutex 不能跨线程拷贝或移动,别传给 lambda 捕获值
std::mutex 是非拷贝、非移动类型,试图把它放进 std::thread 构造函数或 lambda 捕获列表时写成 [=] 或 [mtx],编译直接报错:
error: use of deleted function ‘std::mutex::mutex(const std::mutex&)’
正确方式只有两种:
- 捕获引用:
[&mtx]{ ... mtx.lock(); ... }(注意确保mtx生命周期长于线程) - 传指针或引用到
std::thread:std::thread{f, &mtx},函数参数声明为std::mutex& mtx - 如果用
std::async或线程池,推荐把 mutex 定义在共享数据结构内部,由该结构的方法封装同步逻辑
性能敏感场景慎用 mutex,优先考虑无锁替代方案
每次 lock() 都可能触发系统调用或总线争抢,尤其在高并发、短临界区(比如计数器++)下,std::mutex 成为瓶颈。
可选更轻量的方案:
- 原子操作:
std::atomic—— 无需锁,CPU 硬件保证,适合简单读写counter{0}; counter.fetch_add(1); - 读多写少:用
std::shared_mutex(C++17)或第三方folly::SharedMutex - 彻底避免共享:每个线程用局部变量统计,最后归并(如
tbb::parallel_for的 reduction)
但注意:原子操作不能替代锁来保护复杂不变式(例如“检查余额 > 扣款”这种两步操作),这时候仍需 mutex。
真正容易被忽略的是:mutex 的粒度。锁整个容器不如锁单个元素;锁一次处理 1000 条不如分批加锁。粗粒度锁看着简单,往往是性能隐形杀手。










