c++中如何使用mutex互斥锁_c++线程安全与锁机制详解

必须用 RAII(如 std::lock_guard 或 std::unique_lock)管理 std::mutex,禁用手动 lock/unlock;多锁需用 std::lock 或 std::scoped_lock 避免死锁;mutex 不可复制/移动;锁粒度宜细,临界区忌 I/O 与耗时操作。

std::mutex 必须配合 std::lock_guard 或 std::unique_lock 使用

直接调用 mutex.lock()mutex.unlock() 极易出错:忘记 unlock、异常跳过 unlock、重复 unlock 都会导致未定义行为。C++ 标准库不鼓励手动管理锁生命周期。

正确做法是依赖 RAII —— 用 std::lock_guard(作用域自动加锁/解锁)或 std::unique_lock(支持延迟锁定、转移所有权、条件变量配合)。

  • std::lock_guard 更轻量,构造即加锁,析构即解锁,不可复制,适合简单临界区
  • std::unique_lock 功能更全,但有轻微开销;若只用默认构造,它不持有锁,需显式调用 lock()
  • 绝不能把 std::lock_guard 声明在 if 分支或循环内却期望保护外层逻辑 —— 作用域决定生命周期

多个 mutex 同时加锁必须用 std::lock 避免死锁

当一段逻辑需要同时持有两个或以上 std::mutex 时,如果分别调用 m1.lock()m2.lock(),线程 A 先锁 m1 后等 m2,线程 B 先锁 m2 后等 m1,就会死锁。

标准解法是用 std::lock(m1, m2) —— 它使用“避免死锁算法”(如按地址排序尝试加锁),再配合 std::adopt_lock 构造 std::unique_lock

std::mutex m1, m2;
std::lock(m1, m2); // 原子性获取两个锁
std::unique_lock guard1(m1, std::adopt_lock);
std::unique_lock guard2(m2, std::adopt_lock);
  • 不能对已 lock 的 mutex 再传给 std::lock,否则行为未定义
  • std::scoped_lock(C++17 起)是更简洁的替代:直接 std::scoped_lock<:mutex std::mutex> lock(m1, m2);,内部已调用 std::lock

std::mutex 不可复制、不可移动,成员变量声明要小心

std::mutex 是 move-only 类型(C++11 起禁用拷贝,也不提供移动构造/赋值),因此不能出现在需要拷贝的上下文中:

  • 类中声明 std::mutex mtx; 没问题,但该类自动删除拷贝构造函数和拷贝赋值运算符
  • 若类需可拷贝,不能把 std::mutex 作为直接成员;可改用 std::shared_ptr<:mutex>(注意共享本身不解决线程安全,只是绕过拷贝限制)
  • vector<:mutex> 编译失败;要用 std::vector<:unique_ptr>> 或预分配后 emplace_back
  • lambda 捕获 [mtx = std::move(mtx)] 无效 —— std::mutex 不可移动,捕获只能用引用或指针

性能陷阱:锁粒度太粗或临界区含阻塞操作

常见低效模式是把整个函数体包进一个 std::lock_guard,尤其当临界区内含 I/O、sleep、网络调用或复杂计算时,会严重拖慢并发吞吐。

应只保护真正共享数据读写的部分,其余可并发执行:

void process_data() {
    int local_val = expensive_computation(); // ✅ 可并发执行
    {
        std::lock_guard lock(mtx);
        shared_counter += local_val; // ✅ 仅此处需互斥
        update_log(shared_counter);  // ⚠️ 若 update_log 是 I/O,应移出临界区
    }
    log_to_file("processed"); // ✅ 移出后,多线程可并行写日志
}
  • 临界区内避免 std::coutprintf、文件写入、锁其他资源(如另一个 mutex)
  • 若必须在临界区做耗时操作,考虑用无锁结构(如 std::atomic)、读写锁(std::shared_mutex)或消息队列解耦
  • 调试时加锁日志(如 “enter critical section”)本身会放大竞争,上线前务必移除
锁本身不解决所有线程安全问题;它只是工具。真正容易被忽略的是:共享状态的设计是否必要、数据是否真的需要跨线程修改、能否用线程局部存储(thread_local)或消息传递替代共享。(mutex 只管“谁在用”,不管“用得对不对”)