互斥锁与 RAII

内容介绍

多个线程同时读写同一份数据,如果没有同步,就会产生数据竞争。数据竞争是未定义行为。

std::mutex 用来保护临界区。现代 C++ 不建议手写 lock() / unlock(),而是用 RAII 对象管理锁。

lock_guard

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
 
int main() {
    int counter = 0;
    std::mutex m;
    std::vector<std::thread> threads;
 
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back([&] {
            for (int j = 0; j < 10000; ++j) {
                std::lock_guard<std::mutex> lock(m);
                ++counter;
            }
        });
    }
 
    for (auto& t : threads) {
        t.join();
    }
 
    std::cout << counter << '\n';
}

unique_lock

std::unique_locklock_guard 更灵活,支持延迟加锁、提前解锁、配合条件变量。

std::unique_lock<std::mutex> lock(m);
// do protected work
lock.unlock();
// do unprotected work

最佳代码实践

  • 优先使用 std::lock_guard,需要条件变量或手动解锁时使用 std::unique_lock
  • 锁保护的是数据,不是代码。先明确“哪个 mutex 保护哪个数据”。
  • 锁的作用域尽量小。
  • 多把锁一起加时,用 std::scoped_lock 避免死锁。

常见错误用法

m.lock();
shared_data.push_back(1);
m.unlock();

问题:中间如果抛异常,锁不会释放。

std::lock_guard<std::mutex> lock(m);
slow_io();

问题:持锁期间做慢 IO,会让其他线程长时间阻塞。

注意事项

  • mutex 不能复制。
  • 同一个线程重复锁普通 std::mutex 会死锁。
  • recursive_mutex 不是常规解法,通常说明职责边界需要重新设计。
  • 使用锁不能自动保证整体逻辑正确,尤其要小心 check-then-act。

学习路径