线程创建与 join/detach 之外的另一半问题:共享数据怎么同步。覆盖数据竞争、互斥锁的三种用法、条件变量生产者-消费者、原子操作与死锁。

核心概念速查

概念说明关键点
互斥锁同一时刻只允许一个线程进入std::mutex
lock_guardRAII 自动加解锁推荐,防遗漏
unique_lock更灵活的 RAII 锁可手动解锁、可条件变量
条件变量线程间的等待/通知std::condition_variable
原子操作无锁的线程安全std::atomic
数据竞争多线程同时访问共享数据未定义行为,必须防

为什么需要锁

// 无锁: 两个线程同时对 counter++ 可能丢失更新
// counter++ 实际是"读-改-写"三步,可能交错
int counter = 0;

void add() {
    for (int i = 0; i < 100000; ++i)
        ++counter;   // 数据竞争!
}

互斥锁三种用法

#include <mutex>

std::mutex g_mutex;

// 方式1: 原始 lock/unlock(容易忘解锁,不推荐)
void f1() {
    g_mutex.lock();
    // ... 临界区
    g_mutex.unlock();   // 忘写或中间 return 就死锁
}

// 方式2: lock_guard (推荐,作用域结束自动解锁)
void f2() {
    std::lock_guard<std::mutex> guard(g_mutex);
    // ... 临界区
}   // 这里自动 unlock

// 方式3: unique_lock (需要手动解锁/延迟加锁时)
void f3() {
    std::unique_lock<std::mutex> lock(g_mutex);
    // ... 临界区
    lock.unlock();
    // ... 不需要锁的部分
}

条件变量:生产者-消费者

#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;

// 生产者
void producer() {
    for (int i = 0; i < 10; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        q.push(i);
        cv.notify_one();   // 通知一个等待的消费者
    }
}

// 消费者
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        // wait 会: 释放锁等待 → 被唤醒 → 重新加锁
        cv.wait(lock, []{ return !q.empty(); });
        int val = q.front();
        q.pop();
        lock.unlock();
        // 处理 val...
    }
}

原子操作

#include <atomic>

std::atomic<int> counter{0};
// counter++ 是原子操作,无需加锁
// 适用场景: 计数器、标志位、自旋

// 对比:
// 普通 int  ++   → 需 mutex
// atomic    ++   → 无锁,更快

死锁

死锁四个必要条件:
1. 互斥        资源只能一个线程占用
2. 持有并等待  拿着一个锁还想要另一个
3. 不可剥夺    别人不能抢走已持有的锁
4. 循环等待    两个线程彼此持有对方要的锁

例子:
线程A持锁1等锁2 + 线程B持锁2等锁1 → 死锁

避免: 全局加锁顺序一致 / 用 std::lock 一次锁多个

常见坑

问题原因解决
死锁加锁顺序不一 / 忘解锁固定顺序 / std::lock / RAII
假唤醒条件变量被无故唤醒wait 用谓词条件,循环检查
性能差锁粒度太粗缩小临界区 / 用原子
数据竞争忘记加锁一律 RAII 锁,变量尽量用 atomic
忙等耗 CPU自旋锁滥用用条件变量阻塞等待

顿悟

  • 锁保护的是「数据」,不是「代码」:所有访问同一个共享变量的地方都要用同一把锁,漏一处就出 bug。
  • RAII 是防呆设计:lock_guard 把"忘解锁"从可能性变成不可能,这是 C++ 现代写法核心。
  • 原子 ≠ 万能:只能处理单个变量的简单操作;涉及多个变量的一致性仍需锁。
  • 条件变量的 wait 里一定有循环:防止假唤醒,这是教科书写法,别省。

自测清单

  • 理解数据竞争为什么会丢更新
  • 会用 std::mutex + std::lock_guard
  • 区分 lock_guard 与 unique_lock 的适用场景
  • 能写生产者-消费者(条件变量)
  • 理解死锁四条件,知道如何避免
  • 知道什么时候用 std::atomic