线程创建与 join/detach 之外的另一半问题:共享数据怎么同步。覆盖数据竞争、互斥锁的三种用法、条件变量生产者-消费者、原子操作与死锁。
核心概念速查#
| 概念 | 说明 | 关键点 |
|---|
| 互斥锁 | 同一时刻只允许一个线程进入 | std::mutex |
| lock_guard | RAII 自动加解锁 | 推荐,防遗漏 |
| 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