C++ 专家(17):线程同步深入——C++20 屏障、闩锁、信号量、原子操作内存序
更新时间:2026-09-01。本文是
languages/cpp/expert/专家层第 17 篇,接 SIMD 自动向量化。C++20 新增了std::barrier、std::latch、std::counting_semaphore,补充了之前的std::mutex和std::condition_variable。理解原子操作的内存序才是写好并发代码的关键。
本文要回答的问题
- barrier 和 latch 有什么区别?什么时候用 barrier?
- 信号量怎么用?比条件变量更轻量吗?
- 原子操作的内存序(relaxed、acquire、release、acq_rel、seq_cst)分别是什么?
- 无锁编程的原则是什么?什么时候用?
一、barrier 和 latch
cpp
#include <latch>
#include <barrier>
// latch:一次性使用,所有线程到达后一起继续
std::latch work_done(5); // 需要 5 个线程到达
// 每个线程完成任务后
work_done.count_down(); // 计数减 1
// 主线程等待所有线程完成
work_done.wait(); // 阻塞直到计数为 0
// barrier:可重复使用,所有线程到达后一起继续
std::barrier sync(5, []() {
std::cout << "所有线程到达同步点\n";
});
// 每个线程在同步点调用
sync.arrive_and_wait(); // 等待所有线程到达
// 可以重复使用区别:
latch:一次性,计数到 0 后不能再重置barrier:可重复使用,每次到达后重置
二、信号量
cpp
#include <semaphore>
// 计数信号量:控制资源数量
std::counting_semaphore<5> sem(3); // 最多 5 个,初始 3 个可用
// 获取资源
sem.acquire(); // 阻塞直到有可用资源
// 使用资源...
sem.release(); // 释放资源,计数加 1
// 二进制信号量(互斥量)
std::binary_semaphore mutex(1); // 同 counting_semaphore<1>
// 带超时
if (sem.try_acquire_for(std::chrono::seconds(1))) {
// 获取成功
}和条件变量的对比: 信号量更轻量,不需要锁和谓词检查。
三、原子操作内存序
cpp
#include <atomic>
std::atomic<int> x{0};
std::atomic<int> y{0};
// 线程 1
x.store(1, std::memory_order_release); // 释放语义
y.store(1, std::memory_order_relaxed); // 无约束
// 线程 2
int a = y.load(std::memory_order_relaxed);
int b = x.load(std::memory_order_acquire); // 获取语义内存序含义:
| 内存序 | 含义 | 成本 |
|---|---|---|
relaxed | 无约束,只保证原子性 | 最低 |
acquire | 读操作,后续读写不能重排到前面 | 中等 |
release | 写操作,前面读写不能重排到后面 | 中等 |
acq_rel | acquire + release | 较高 |
seq_cst | 全局顺序一致 | 最高 |
经验: 默认是 seq_cst,最容易理解但最慢。大多数场景用 acquire/release 就够了。
四、无锁编程基础
cpp
// 无锁栈:用原子操作
template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head{nullptr};
public:
void push(T value) {
Node* node = new Node{std::move(value), nullptr};
Node* old_head = head.load(std::memory_order_relaxed);
do {
node->next = old_head;
} while (!head.compare_exchange_weak(
old_head, node,
std::memory_order_release,
std::memory_order_relaxed));
}
bool pop(T& value) {
Node* old_head = head.load(std::memory_order_relaxed);
do {
if (old_head == nullptr) return false;
} while (!head.compare_exchange_weak(
old_head, old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed));
value = std::move(old_head->data);
delete old_head;
return true;
}
};无锁编程原则:
- 用
compare_exchange_weak而不是compare_exchange_strong(性能更好) - ABA 问题:用 tagged pointer 或 hazard pointer 解决
- 内存序尽量放松,用 acquire/release 而不是 seq_cst
- 无锁数据结构极少写,优先用锁
五、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 内存序用错 | 数据竞争,奇怪的 bug | 内存序不熟悉就用 seq_cst |
| ABA 问题 | 值变回原来的,CAS 成功 | 用 tagged pointer 加版本号 |
| 无锁数据结构内存泄漏 | 节点删除时其他线程还在读 | 用 hazard pointer 或 epoch-based reclamation |
| latch 计数小于 0 | UB | 确保 count_down 不超过初始计数 |
相关与延伸
下一篇:协程机制深入——awaitable、promise 类型、调度器实现;C++ 并发入门,见 C++ 并发编程。
一句话总结
C++ 线程同步深入:barrier 可重复使用(所有线程到达同步点),latch 一次性(计数到 0 后不可重置);信号量比条件变量更轻量,acquire/release 控制资源;原子操作内存序从 relaxed(无约束)到 seq_cst(全局一致),acquire/release 组合实现线程间同步;无锁编程用 compare_exchange_weak 实现 lock-free 数据结构,注意 ABA 问题;内存序不熟悉就用默认 seq_cst。