C++ 线程同步
线程同步是多线程编程的核心,旨在解决数据竞争 与执行顺序协调 问题。本文将快速回顾基础,重点聚焦高阶用法(内存序、无锁编程、C++20 新特性等)。
第一部分:基础快速回顾
1. 互斥锁 (std::mutex)
最基础的同步原语,通过 lock()/unlock() 保护临界区。
cpp
#include <mutex>
std::mutex mtx;
void safe_increment(int& x) {
mtx.lock();
++x; // 临界区
mtx.unlock();
}
2. 条件变量 (std::condition_variable)
用于线程间执行顺序协调 ,配合 std::unique_lock 使用。
cpp
#include <condition_variable>
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
// 执行任务
}
3. 原子操作 (std::atomic)
无需锁的轻量级同步,保证操作的原子性。
cpp
#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1); // 原子自增
第二部分:锁的高阶封装与类型
1. std::lock_guard:RAII 简单锁
自动管理锁的生命周期,不可移动、不可提前解锁。
cpp
{
std::lock_guard<std::mutex> guard(mtx);
// 临界区
} // 自动解锁
2. std::unique_lock:灵活锁
支持移动、提前解锁、延迟加锁,是条件变量的标配。
cpp
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
// ... 其他操作 ...
lock.lock(); // 手动加锁
lock.unlock(); // 提前解锁
3. std::shared_mutex + std::shared_lock:读写锁
实现多读单写(Readers-Writer Lock),读操作共享锁,写操作独占锁。
cpp
#include <shared_mutex>
std::shared_mutex rw_mtx;
void read() {
std::shared_lock<std::shared_mutex> lock(rw_mtx); // 共享锁(读)
// 读操作
}
void write() {
std::unique_lock<std::shared_mutex> lock(rw_mtx); // 独占锁(写)
// 写操作
}
4. std::scoped_lock(C++17):多锁同时加锁
避免死锁的利器,可同时对多个互斥锁原子性加锁。
cpp
std::mutex m1, m2;
{
std::scoped_lock lock(m1, m2); // 同时加锁 m1 和 m2,自动避免死锁
// 临界区
} // 自动解锁
第三部分:条件变量的深度解析
1. 虚假唤醒(Spurious Wakeup)
条件变量可能被操作系统虚假唤醒 ,必须用 while 循环检查条件。
cpp
cv.wait(lock, []{ return ready; }); // 等价于:
// while (!ready) { cv.wait(lock); }
2. 生产者-消费者模型(标准实现)
cpp
std::queue<int> q;
const int MAX_SIZE = 10;
void producer(int val) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return q.size() < MAX_SIZE; }); // 队列不满
q.push(val);
cv.notify_one(); // 通知消费者
}
int consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !q.empty(); }); // 队列不空
int val = q.front();
q.pop();
cv.notify_one(); // 通知生产者
return val;
}
第四部分:原子操作与内存序(核心高阶)
std::atomic 的默认内存序是 std::memory_order_seq_cst(顺序一致性),但通过调整内存序可优化性能。
1. 六种内存序
| 内存序 | 含义 | 适用场景 |
|---|---|---|
memory_order_relaxed |
仅保证原子性,不保证顺序 | 计数器、统计信息 |
memory_order_acquire |
读操作:阻止后续读写重排到本操作之前 | 消费者(读取数据) |
memory_order_release |
写操作:阻止前面读写重排到本操作之后 | 生产者(写入数据) |
memory_order_acq_rel |
同时具有 acquire 和 release 语义 |
读写混合操作 |
memory_order_seq_cst |
默认,全局顺序一致性(所有线程看到一致的操作顺序) | 最严格、最安全(性能稍低) |
2. 示例:Acquire-Release 实现生产者-消费者
cpp
std::atomic<int> data{0};
std::atomic<bool> ready{false};
void producer() {
data.store(42, std::memory_order_relaxed); // 写入数据
ready.store(true, std::memory_order_release); // 释放:确保 data 先写入
}
void consumer() {
while (!ready.load(std::memory_order_acquire)); // 获取:等待 ready
assert(data.load(std::memory_order_relaxed) == 42); // 保证读到 data
}
3. 内存序选择原则
- 优先用
memory_order_seq_cst(默认),确保正确性; - 性能瓶颈时,对生产者-消费者 模型用
acquire-release; - 仅对独立计数器用
memory_order_relaxed。
第五部分:无锁编程进阶
无锁编程(Lock-Free)通过原子操作避免锁开销,但复杂度极高,仅在性能关键场景使用。
1. CAS(Compare-And-Swap)操作
无锁编程的核心,通过 compare_exchange_weak/compare_exchange_strong 实现。
cpp
std::atomic<int> val{0};
void lock_free_increment() {
int old_val = val.load(std::memory_order_relaxed);
// 循环尝试 CAS,直到成功
while (!val.compare_exchange_weak(old_val, old_val + 1,
std::memory_order_acq_rel));
}
2. 无锁队列(解决 ABA 问题)
ABA 问题:值从 A→B→A,CAS 会误判为未改变。解决方案:带版本号的指针。
cpp
template<typename T>
struct Node {
T data;
Node* next;
Node(T val) : data(val), next(nullptr) {}
};
std::atomic<Node<int>*> head{nullptr};
void lock_free_push(int val) {
Node<int>* new_node = new Node<int>(val);
new_node->next = head.load(std::memory_order_relaxed);
// CAS 更新头指针
while (!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_acq_rel));
}
3. 无锁编程的挑战
- ABA 问题(用版本号或
std::atomic_shared_ptr解决); - 内存回收(无锁环境下难以安全释放内存,需用 Hazard Pointers 或 Epoch-Based Reclamation);
- 可移植性差(依赖硬件原子指令支持)。
第六部分:C++20 线程同步新特性
1. std::counting_semaphore:信号量
控制同时访问资源的线程数量。
cpp
#include <semaphore>
std::counting_semaphore<3> sem(3); // 最多 3 个线程同时访问
void worker() {
sem.acquire(); // 获取信号量
// 访问资源
sem.release(); // 释放信号量
}
2. std::latch:单次使用的线程屏障
等待 N 个线程到达,不可重用。
cpp
#include <latch>
std::latch work_latch(5); // 等待 5 个线程
void worker() {
// 执行任务
work_latch.count_down(); // 计数减 1
work_latch.wait(); // 等待计数为 0
}
3. std::barrier:可重用的线程屏障
等待 N 个线程到达,到达后可执行回调,可重用。
cpp
#include <barrier>
std::barrier sync_barrier(5, []{ /* 所有线程到达后执行的回调 */ });
void worker() {
for (int i = 0; i < 10; ++i) {
// 执行第 i 轮任务
sync_barrier.arrive_and_wait(); // 等待其他线程
}
}
第七部分:性能优化与最佳实践
1. 锁粒度的选择
- 粗粒度锁:整个操作加一把锁(简单,但竞争大);
- 细粒度锁:拆分临界区,用多把锁(复杂,但竞争小)。
2. 死锁避免策略
- 按固定顺序加锁(如按锁的地址从小到大);
- 用
std::scoped_lock(C++17)或std::lock同时加锁; - 避免在持有锁时调用用户代码(防止回调中再次加锁)。
3. 减少锁竞争的技巧
- 用线程局部存储 (
thread_local)避免共享; - 用读写锁 (
std::shared_mutex)优化多读场景; - 用无锁数据结构 (如
boost::lockfree)替代锁。
4. 调试与分析工具
- ThreadSanitizer(TSan):检测数据竞争与死锁;
- perf:分析性能瓶颈;
- gdb :调试多线程程序(
info threads、thread apply)。