不能直接用 std::queue 做无锁 spsc 队列,因其底层依赖 std::deque 或 std::vector,push/pop 非原子、有共享状态,且不满足生产者只写 tail、消费者只读 head 的内存访问隔离前提;必须用 std::atomic 管理 head/tail、2 的幂容量环形缓冲区,并严格配对 acquire/release 内存序。

为什么不能直接用 std::queue 做无锁 SPSC 队列
因为 std::queue 底层依赖 std::deque 或 std::vector,其 push/pop 操作不是原子的,且内部有共享状态(如 size、迭代器维护),无法规避锁或内存重排。更关键的是:它不保证对 head/tail 的读写是分离的——而 SPSC 队列的核心前提,就是生产者只改 tail、消费者只改 head,二者内存访问完全不交叉。
所以必须手动管理两个原子整数(head、tail)+ 一块预分配的环形缓冲区,并严格控制内存序。
-
head和tail必须是std::atomic<size_t></size_t>,不能用普通int+volatile——后者不提供原子性,也不约束编译器/CPU 重排 - 缓冲区大小必须是 2 的幂(例如 1024),才能用位运算快速取模:
index & (capacity - 1),避免除法开销 - 队列“满”的判断不能用
tail == head(这和“空”冲突),得预留一个空槽,即实际可用容量为capacity - 1
如何用指针 + 原子变量实现入队(enqueue)
入队由唯一生产者执行,只需操作 tail,但必须确保:1)拿到空闲槽位;2)写入数据;3)推进 tail。三步缺一不可,且第 2 步不能被重排到第 3 步之后。
bool enqueue(const T& data) {
const size_t tail = tail_.load(std::memory_order_relaxed);
const size_t next_tail = (tail + 1) & mask_;
if (next_tail == head_.load(std::memory_order_acquire)) {
return false; // full
}
buffer_[tail & mask_] = data;
tail_.store(next_tail, std::memory_order_release);
return true;
}
-
tail_.load(std::memory_order_relaxed)可以用 relaxed:生产者不关心其他线程的 head 变化,只读自己上次写的值 -
head_.load(std::memory_order_acquire)必须是 acquire:防止编译器/CPU 把后续的buffer_[...]写入重排到该 load 之前 -
tail_.store(..., std::memory_order_release)是 release:确保前面的buffer_[tail & mask_] = data在 tail 更新前完成,让消费者能安全读到新数据 - 注意:这里没用
compare_exchange_weak,因为 SPSC 场景下tail不会竞争,直接 store 更高效
出队(dequeue)时怎么避免 ABA 和脏读
消费者只动 head,但必须确认对应槽位确实已被生产者写完。常见错误是先读 head、再读 buffer[head]、最后更新 head,中间若生产者刚好把新数据写进同一位置(ABA),就会读到旧值。
正确做法是:先读 head,再检查 head != tail(说明有数据),再读数据,最后用 release-store 更新 head:
bool dequeue(T* out) {
const size_t head = head_.load(std::memory_order_relaxed);
if (head == tail_.load(std::memory_order_acquire)) {
return false; // empty
}
*out = buffer_[head & mask_];
head_.store((head + 1) & mask_, std::memory_order_release);
return true;
}
- 两次 load 的 memory order 不同:第一次 relaxed(仅本地使用),第二次 acquire(同步 tail 的最新值)
- 不能把
tail_.load提前到函数开头并复用——SPSC 允许这样优化,但一旦未来扩展成 MPSC 就崩了;保持逻辑清晰比省一次 load 更重要 - 如果
T是非 trivially copyable 类型(比如含虚函数或自定义析构),需用 placement new + 显式调用 destructor,否则可能跳过资源清理
哪些细节会导致性能断崖或未定义行为
最常被忽略的是缓存行伪共享(false sharing):如果 head_ 和 tail_ 在同一缓存行,生产者写 tail_ 会让消费者所在 CPU 的缓存行失效,强制重新加载整个行(含 head_),即使 head_ 没变。
- 必须用
alignas(64)分别对齐head_和tail_(假设缓存行 64 字节),或者在它们之间填充 56 字节 - 缓冲区
buffer_要用new T[capacity]分配,不能用std::vector<t></t>——后者 operator[] 不保证无异常,且可能触发额外分支判断 - 构造函数里必须显式初始化所有原子变量:
head_(0), tail_(0),否则未定义行为(原子变量不自动零初始化) - 如果队列用于实时系统(如音频处理),要禁用内存页换入换出:
mlock()或VirtualLock(),否则缺页中断会打破无锁承诺
真正难的从来不是写对那几行原子操作,而是让 head/tail/缓冲区三者在硬件层面互不干扰——CPU 缓存、内存控制器、编译器优化,每一层都可能悄悄破坏你的“无锁”假设。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











