不能直接用 std::queue 实现无锁 SPSC,因其底层依赖 std::deque 或 std::vector,push/pop 非原子、指针易失效,且缺乏原子索引控制;无锁 SPSC 必须用固定容量环形缓冲区、两个 std::atomic 索引(head/tail)、2 的幂长度、acquire/release 内存序及位掩码运算。

为什么不能直接用 std::queue 实现无锁 SPSC
因为 std::queue 内部依赖 std::deque 或 std::vector,其 push/pop 操作不是原子的,且迭代器/指针可能失效;更关键的是,它没提供对底层存储的原子索引控制——而无锁 SPSC 的核心恰恰是两个线程各自独占一个原子整数(head 和 tail),靠 CAS + 内存序协调,不依赖互斥量。
常见错误是试图“包装”标准容器加原子计数器,结果在容量边界处出现 ABA 问题或虚假失败,尤其在高吞吐下丢数据或死循环。
- 必须使用固定大小、连续内存(如
std::array或new T[N]) -
head和tail必须是std::atomic<size_t></size_t>,且所有读写都需搭配合适的内存序(通常是memory_order_acquire/memory_order_release) - 缓冲区长度必须是 2 的幂(便于用位掩码替代取模,避免分支和除法)
如何正确实现 push 和 pop 的原子逻辑
SPSC 场景下,生产者只改 tail,消费者只改 head,所以无需双向 CAS。但要注意:push 前要先读 head 判断是否满,pop 前要先读 tail 判断是否空——这两个读操作必须是 acquire 序,确保看到最新状态;而更新 tail 或 head 必须是 release 序,保证本线程对缓冲区元素的写入已对另一线程可见。
典型实现中,用 (tail.load(acquire) - head.load(acquire)) & (capacity - 1) 算当前占用长度(注意:不是 tail - head,因为会绕圈);判断满用 (tail + 1) & (capacity - 1) == head,比 “长度 >= capacity-1” 更安全(避免因编译器重排导致误判)。
立即学习“C++免费学习笔记(深入)”;
// 示例:非阻塞 push
bool push(const T& item) {
const size_t tail = tail_.load(std::memory_order_acquire);
const size_t next_tail = (tail + 1) & mask_;
if (next_tail == head_.load(std::memory_order_acquire)) return false;
buffer_[tail] = item;
tail_.store(next_tail, std::memory_order_release);
return true;
}
内存序选错会导致什么?
最常踩的坑是把 tail_.load() 写成 relaxed——这会让编译器或 CPU 把后续对 buffer_[tail] 的写入重排到 load 之前,消费者可能读到未初始化的值;同样,tail_.store() 若用 relaxed,消费者 load head 后可能看不到刚写入的元素内容。
- 所有用于同步“数据就绪”的 load 必须是
acquire(包括检查空/满时读对方索引) - 所有用于通知“数据已就绪”的 store 必须是
release(更新自己索引时) - 不需要
seq_cst——它太重,且在 x86 上隐含 full barrier,浪费性能 - 如果 T 类型含非平凡构造/析构(如
std::string),务必用 placement new + 显式调用 destructor,否则对象生命周期失控
实际部署时容易被忽略的细节
环形缓冲区本身不处理内存分配——你得自己管好 buffer_ 的生命周期:它必须在线程整个运行期有效,不能是栈变量或临时对象;另外,SPSC 不代表完全零开销:cache line 伪共享仍会发生,若 head_ 和 tail_ 在同一 cache line,生产者和消费者频繁更新会引发 cache bounce。
- 把
head_、tail_和 padding 放到不同 cache line(例如各加 64 字节填充) - 禁用编译器对 buffer 数组的自动向量化(某些场景下会破坏原子性假设)
- 调试时可加 assert:
assert((tail_ & mask_) == tail_);验证索引始终在范围内 - 不要尝试扩展为 MPSC/SPMC——那需要复杂得多的算法(如 Harris 链表或双字 CAS),不是简单改几个原子变量能解决的


















