直接用 std::atomic 操作下标不安全,因存在 ABA 问题和读写重排;正确做法是用 CAS 循环配合内存序约束或单原子变量编码双下标,SPSC 场景可免 CAS 实现 wait-free。

为什么直接用 std::atomic 操作下标不够安全
很多人以为只要把 head 和 tail 声明为 std::atomic<int></int>,再用 load()/store() 就能实现无锁队列——但实际会遇到 ABA 问题和读写重排。比如消费者读到旧的 tail 值后,生产者已完成一次完整 push-pop-push 循环,tail 回到相同数值,消费者误判“有数据可读”,结果读到未初始化内存。
真正安全的做法是:用单个原子变量承载两个逻辑下标(或用 std::atomic<uint64_t></uint64_t> 拆分为高低 32 位),或采用 std::atomic<int>::compare_exchange_weak()</int> 配合循环重试。更推荐后者,兼容性好、语义清晰。
- 避免裸用
operator++或fetch_add()直接更新下标——它不检查队列边界与状态一致性 - 所有读写操作必须先计算目标索引,再用 CAS 原子更新下标,失败则重算
- 编译器可能对非 volatile 的普通变量做重排,需在关键路径插入
std::atomic_thread_fence(std::memory_order_acquire)或std::memory_order_release
如何用 std::atomic<uint32_t></uint32_t> 实现单生产者单消费者(SPSC)场景下的零开销同步
SPSC 是唯一能完全避免 CAS 重试的场景,可做到真正 wait-free。核心思路是:生产者只改 tail,消费者只改 head,二者互不干扰;靠内存序约束保证数据可见性。
示例关键片段:
立即学习“C++免费学习笔记(深入)”;
class spsc_ringbuffer {
static constexpr size_t CAPACITY = 1024;
std::array<int, CAPACITY> buf_;
std::atomic<uint32_t> head_{0}; // consumer side
std::atomic<uint32_t> tail_{0}; // producer side
<p>public:
bool try_push(int val) {
const uint32<em>t tail = tail</em>.load(std::memory_order_relaxed);
const uint32_t next_tail = (tail + 1) & (CAPACITY - 1);
if (next<em>tail == head</em>.load(std::memory_order<em>acquire)) return false; // full
buf</em>[tail] = val;
tail_.store(next_tail, std::memory_order_release); // publish data
return true;
}</p><pre class='brush:php;toolbar:false;'>bool try_pop(int& out) {
const uint32_t head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire)) return false; // empty
out = buf_[head];
const uint32_t next_head = (head + 1) & (CAPACITY - 1);
head_.store(next_head, std::memory_order_release);
return true;
}};
-
CAPACITY必须是 2 的幂,才能用位与代替取模,且head == tail表示空,(tail + 1) & mask == head表示满 -
std::memory_order_acquire在读head/tail时确保能看到之前所有写入的数据 -
std::memory_order_release在写head/tail时确保本线程此前所有写操作对另一方可见 - 不能省略
relaxedload —— 它只是读下标,不需要同步语义;加 acquire 会拖慢性能
多生产者/多消费者(MPMC)为何必须用 CAS 且容易卡住
MPMC 下多个线程可能同时修改同一变量(如 tail),必须用 compare_exchange_weak() 保证原子性。但 CAS 失败后需重试,若竞争激烈,线程可能反复失败、空转消耗 CPU。
典型陷阱是:CAS 更新下标前没校验容量是否足够,导致写入越界或覆盖未消费数据。
- 每次 CAS 前必须重新读取当前
tail和head,再计算是否可写——不能复用旧值 - 失败重试时要加短暂 pause(如
_mm_pause()或std::this_thread::yield()),缓解总线争抢 - 不要在 CAS 循环里做复杂计算(如构造对象),否则失败代价过高
- 某些平台(ARMv8+)对
std::atomic的compare_exchange_weak有硬件限制,需确认编译器生成的是 LDAXR/STXR 而非模拟实现
缓存行伪共享(False Sharing)会让无锁队列变慢十倍
如果 head 和 tail 在同一缓存行(通常 64 字节),即使它们被不同线程独占修改,CPU 也会因缓存一致性协议频繁同步整行,造成严重性能抖动。
实操上必须手动对齐并填充:
struct alignas(64) mpmc_node {
std::atomic<uint32_t> head_;
char pad1[64 - sizeof(std::atomic<uint32_t>)];
std::atomic<uint32_t> tail_;
char pad2[64 - sizeof(std::atomic<uint32_t>)];
};- 仅靠
alignas(64)不够——head_和tail_还得各自独占一个缓存行 - 填充字段用
char数组而非std::byte,避免 C++20 以下兼容问题 - 调试时可用
perf record -e cache-misses观察伪共享程度;若 miss rate > 5%,大概率存在此问题
真正难的不是写出 CAS 循环,而是让每个线程只碰自己该碰的缓存行、不让内存屏障变成瓶颈、并在调试器里一眼看出哪个下标卡住了——这些细节不跑真实负载根本暴露不出来。


















