不能直接用std::queue做无锁队列,因为它底层非原子、无内存序控制、不支持环形缓冲区原语;真正可行的是手写环形缓冲区+原子索引+acquire/release内存序,且缓冲区大小须为2的幂。

为什么不能直接用 std::queue 做无锁队列
因为 std::queue 底层依赖 std::deque 或 std::vector,所有操作都带内部互斥或非原子内存访问,根本不是无锁(lock-free)的。更关键的是,它没提供原子读写索引、内存序控制、以及环形缓冲区所需的边界处理原语——这些才是无锁 SPSC 队列的骨架。
真正可行的起点是手写环形缓冲区 + 原子索引 + 显式内存序。核心就三样:std::atomic<size_t></size_t> 管理读写位置,memory_order_acquire/memory_order_release 控制可见性,以及对缓冲区大小强制 2 的幂次(方便用位运算取模)。
std::atomic 索引怎么初始化和更新才安全
两个原子变量:m_head(消费者读位置)、m_tail(生产者写位置),初始都设为 0。更新时绝不能用 fetch_add(1) 后再算下标——这会导致 ABA 问题或越界读写。
- 每次读/写前先用
load(memory_order_acquire)获取当前索引 - 计算实际下标用位与:
index = atomic_val & (buffer_size - 1)(前提是buffer_size是 2 的幂) - 更新后用
store(new_val, memory_order_release)发布新位置 - 注意:不能用
memory_order_relaxed做 load/store,否则编译器或 CPU 可能重排指令,导致读到旧数据
如何避免伪共享(false sharing)和缓存行冲突
生产者和消费者各自只改一个索引,但若 m_head 和 m_tail 在同一缓存行(通常 64 字节),频繁修改会触发缓存一致性协议开销,性能反而比有锁还差。
立即学习“C++免费学习笔记(深入)”;
解决方案是手动填充:
struct alignas(64) spsc_queue {
std::atomic<size_t> m_head{0};
char pad1[64 - sizeof(std::atomic<size_t>)];
std::atomic<size_t> m_tail{0};
char pad2[64 - sizeof(std::atomic<size_t>)];
T m_buffer[BUFFER_SIZE];
};这样保证 m_head 和 m_tail 各占独立缓存行。别信“编译器自动对齐”,也别用 alignas(128) —— 浪费空间且无收益。
push/pop 的边界检查和内存屏障怎么配对
SPSC 场景下,不需要全局同步,只需确保:生产者写入数据后,消费者才能看到;消费者读完后,生产者才能复用该槽位。靠两组 barrier 配对:
- push 末尾用
store(m_tail, memory_order_release):让之前所有写操作对消费者可见 - pop 开头用
load(m_head, memory_order_acquire):确保能看到生产者 release 写过的数据 - 判断是否为空/满时,用
(m_tail.load() - m_head.load())计算长度(无符号减法天然支持 wraparound) - 切忌在 push 中用
memory_order_acq_rel—— 这是多余开销,SPSC 不需要双向同步
真正的难点不在代码长短,而在验证:你得用 std::atomic_thread_fence 或 TSAN 检查重排,还得在真实多核机器上测吞吐抖动——很多看似正确的实现,在 ARM 或 AMD CPU 上会因内存模型差异暴露问题。



















