C++ 并发与线程安全
更新时间:2026-08-25。本文是
languages/cpp/主题高手层第 4 篇。多核时代,并发是性能的必修课——但也是 bug 的重灾区。本文讲清线程、互斥、原子操作三件事,以及它们和内存模型、缓存一致性的关系。
本文要回答的问题
- 怎么创建线程?多线程为什么需要锁?
mutex、lock_guard、condition_variable分别解决什么问题?- 什么是数据竞争?原子操作和锁有什么区别?
- 为什么说"锁是有成本的",成本从哪来?
一、线程基础
1.1 std::thread
cpp
#include <thread>
void worker(int id) {
printf("thread %d\n", id);
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join(); // 等待 t1 结束
t2.join(); // 等待 t2 结束
return 0;
}std::thread创建新线程,join()等待其结束。- 忘记
join或detach会导致terminate。
1.2 数据竞争(data race)
cpp
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++)
counter++; // 数据竞争!多个线程同时读写 counter
}counter++ 不是原子操作(读-改-写三步),两个线程同时执行会丢失更新。这就是数据竞争——未定义行为,是最难查的并发 bug。
二、互斥锁 mutex
2.1 mutex + lock_guard
cpp
#include <mutex>
std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
counter++; // 临界区
} // 离开作用域自动解锁
}lock_guard 是 RAII 的锁——构造加锁、析构解锁,异常安全。
2.2 死锁(deadlock)
cpp
std::mutex m1, m2;
void f() {
std::lock_guard<std::mutex> l1(m1);
std::lock_guard<std::mutex> l2(m2); // 需要 m1 和 m2
}
void g() {
std::lock_guard<std::mutex> l2(m2);
std::lock_guard<std::mutex> l1(m1); // 顺序相反!
}
// f 拿到 m1 等 m2,g 拿到 m2 等 m1 → 互相等待,死锁预防:
- 固定加锁顺序(所有线程按相同顺序加锁)
- 用
std::lock(m1, m2)同时锁多个(避免死锁)
三、条件变量 condition_variable
用于线程间的"等待通知":
cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 生产者
void produce() {
{
std::lock_guard<std::mutex> lk(mtx);
ready = true;
}
cv.notify_one(); // 通知一个等待者
}
// 消费者
void consume() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, []{ return ready; }); // 等待直到 ready
// 处理数据
}典型场景:生产者-消费者模式、线程池任务调度。
四、原子操作 atomic
4.1 atomic 变量
cpp
#include <atomic>
std::atomic<int> counter{0};
void increment() {
for (int i = 0; i < 100000; i++)
counter++; // 原子操作,无数据竞争,且比加锁快
}原子操作直接在 CPU 层面完成(如 lock add 指令),比互斥锁轻量得多——没有系统调用、没有上下文切换。
4.2 原子 vs 锁
| 维度 | 原子操作 | 互斥锁 |
|---|---|---|
| 粒度 | 单个变量 | 一段代码(临界区) |
| 成本 | 低(一条 CPU 指令) | 高(可能上下文切换) |
| 适用 | 计数器、标志位 | 复杂的多步骤操作 |
| 阻塞 | 无(无锁) | 有(其他线程等待) |
4.3 内存序(memory order)
cpp
counter.fetch_add(1, std::memory_order_relaxed); // 只保证原子性,不保证顺序
counter.fetch_add(1, std::memory_order_seq_cst); // 默认:顺序一致性内存序控制原子操作的"可见性顺序",是理解 CPU 乱序执行、内存屏障的关键,详见 L4 内存屏障。
五、锁的性能成本(衔接主线)
锁为什么有成本?成本来自三方面:

- 缓存行竞争:锁变量本身被多个核反复读写,触发缓存一致性协议(MESI)的来回传递,见 L3 缓存一致性。
- 上下文切换:等锁的线程可能被挂起,切换有开销。
- 串行化:临界区只能一个线程进,多核并行度下降(Amdahl 定律)。
优化方向:无锁数据结构(原子操作)、减小临界区、shard(分段锁)。这也是 量化低延迟 的核心议题。
六、与本站主线衔接
- 缓存一致性:锁和原子操作都依赖 MESI 协议,见 L3 缓存一致性。
- 内存模型:内存序、乱序执行、内存屏障,见 L4 内存屏障。
- 进程与线程:线程创建、上下文切换的 OS 层机制,见 L2 进程与线程。
- 剖析工具:锁竞争热点用
perf或 lockstat 定位。
一句话总结
并发 = thread 创建线程 + mutex/lock_guard 互斥 + condition_variable 等待通知 + atomic 无锁原子操作;锁有成本(缓存竞争/切换/串行化),能用原子操作就别用锁,这是多核时代性能优化的核心。
上一篇:异常安全与错误处理 下一篇:虚函数、虚表与多态机制