std::mutex必须配合RAII锁管理器使用,推荐std::lock_guard或C++17的std::scoped_lock;禁止裸调用lock/unlock,须确保加锁顺序一致以防死锁,且mutex不可跨线程释放。

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_data = 0;
void increment() {
std::lock_guard lock(mtx); // 构造即加锁
++shared_data; // 临界区
} // 析构自动 unlock —— 即使抛异常也安全
-
std::lock_guard轻量、不可复制、不可转移,适合“一进一出”简单场景 -
std::unique_lock更灵活(支持try_lock()、defer_lock、release()),但有轻微开销 - 切勿在多个函数间传递
std::lock_guard对象,它不是锁本身,只是锁的 RAII 封装
多个 mutex 加锁顺序不一致会引发死锁
当多个线程需要同时操作两个共享资源(比如账户 A 和 B 的余额),若各自按不同顺序加锁(线程1先锁 A 再锁 B,线程2先锁 B 再锁 A),就可能互相等待,形成死锁。
解决方法只有一条铁律:所有线程必须以**全局一致的顺序**获取多个 mutex。常见策略:
立即学习“C++免费学习笔记(深入)”;
- 按 mutex 对象地址排序:
std::scoped_lock(C++17 起)自动按地址升序加锁,推荐优先使用 - 手动约定顺序,比如始终按变量声明顺序或 ID 数值大小加锁
- 避免在持有 mutex 期间调用可能再申请其他 mutex 的第三方函数(尤其是非内联、无文档说明线程安全性的函数)
std::mutex mtx_a, mtx_b;
// ✅ 推荐:用 scoped_lock 同时加多个锁,自动避免死锁
void transfer(Account& from, Account& to, int amount) {
std::scoped_lock lock(mtx_a, mtx_b); // 自动按地址顺序加锁
from.balance -= amount;
to.balance += amount;
}
std::mutex 不可复制、不可移动,成员变量需显式初始化
因为 std::mutex 禁用了拷贝和移动构造/赋值,如果你把它作为类成员,又没写构造函数,编译器生成的默认构造函数会尝试调用其默认构造 —— 这没问题;但一旦你写了自定义构造函数却忘了初始化 mutex,就会编译失败。
典型错误写法:
class Counter {
std::mutex mtx;
int value;
public:
Counter(int v) : value(v) {} // ❌ 编译错误:mtx 未被初始化
};
正确写法:
- 用成员初始化器列表显式调用
mtx()(默认构造) - 或直接在声明处默认初始化:
std::mutex mtx{}; - 不要试图
new std::mutex后裸指针管理 —— 容易泄漏,且失去 RAII 优势
class Counter {
std::mutex mtx;
int value;
public:
Counter(int v) : mtx(), value(v) {} // ✅ 显式初始化
// 或:Counter(int v) : value(v) {} // ✅ C++11 起,未显式初始化也会调用默认构造
};
std::mutex 不能跨线程释放,也不能重复 unlock
每个 std::mutex 只能由加锁它的同一线程 unlock,否则行为未定义(常见表现是程序崩溃或断言失败)。这和 pthread_mutex_t 的 PTHREAD_MUTEX_ERRORCHECK 类似,但 C++ 标准不保证报错,更危险。
典型误用场景:
- 把
std::lock_guard对象从一个线程 move 到另一个线程(无效,且违反 RAII 设计) - 在 lambda 中捕获 mutex 并在线程池里调用(lambda 执行在线程池线程上,但 lock_guard 在创建线程上构造)
- 手动调用
unlock()后再次调用(即使没异常)
记住:锁的生命周期和加锁线程强绑定。只要坚持用 std::lock_guard / std::scoped_lock,就能天然规避这些问题 —— 因为它们只在当前栈帧存在,不可能跨线程传递。
真正容易被忽略的是:某些封装类(比如带缓存的单例、对象池)内部用了 mutex,但对外没暴露线程安全契约。调用前务必确认文档,或自己加锁 —— 别假设“它应该线程安全”。











