绝大多数人手写无锁队列大概率出错,因其未深入理解atomic内存序、aba问题、内存重排及指针宽原子性,导致数据竞争和未定义行为;推荐使用boost::lockfree::queue或moodycamel::concurrentqueue等成熟实现。

为什么直接手写无锁队列大概率出错
绝大多数人在没深入理解 std::atomic 内存序、ABA问题、内存重排和指针宽原子性(pointer-width atomic)的前提下,写的“无锁队列”实际是数据竞争未定义行为——它可能在本地测试全过,上线后在特定CPU(如ARM)或高并发压测下随机崩溃或静默丢数据。
真正可用的无锁队列必须满足:所有指针操作是原子的、每次读写有明确的memory_order约束、节点生命周期管理不依赖引用计数(否则又引入锁或RCU开销)。
- 常见错误:用
std::atomic<int></int>模拟指针偏移,但head_.load()和tail_.load()之间节点可能已被回收 → 访问野指针 - 典型误用:只对
next_字段加std::atomic,但节点本身未对齐或未保证缓存行独占 → false sharing + 性能暴跌 - 隐蔽陷阱:在 x86 上看似正常,因为其默认强内存模型掩盖了
memory_order_relaxed的重排问题;换到 ARM/AArch64 就立刻暴露
用 boost::lockfree::queue 是最现实的选择
Boost.Lockfree 提供生产级无锁队列实现,底层已处理节点内存池、ABA防护(通过 tagged_ptr)、跨平台内存序适配。它不依赖 new/delete,而是预分配固定大小的环形缓冲区或带内存池的链表。
使用前提:队列容量可预估(环形模式),或允许最大节点数限制(带池链表模式)。若需动态无限扩容,它会退化为带锁路径(此时不如直接用 std::queue + std::mutex)。
- 环形缓冲区模式(推荐):
boost::lockfree::queue<int> q(1024); // 编译期确定大小,无 new,最快</int>
- 动态节点池模式:
boost::lockfree::queue<int boost::lockfree::capacity>> q;</int>
容量为 0 表示使用内部内存池,默认最多 1024 节点,超限时push()返回false - 注意:它不提供迭代器、
size()是 O(1) 但非精确(因并发读写 head/tail 可能瞬时不一致)
自己实现简易单生产者单消费者(SPSC)无锁队列可行
SPSC 场景下无需处理 ABA 和多写者竞争,可用环形缓冲区 + 原子索引实现极简无锁队列,且能保证精确 size() 和等待空/满的能力(如配合 futex 或条件变量)。
核心是两个 std::atomic<size_t></size_t>:一个 head_(消费者读位置),一个 tail_(生产者写位置),所有操作用 memory_order_acquire/memory_order_release 配对。
- 缓冲区大小必须是 2 的幂(便于位运算取模),例如
static constexpr size_t CAPACITY = 1024; -
push()先读tail_.load(memory_order_acquire),计算新位置,再用compare_exchange_weak更新;失败则重试 - 关键点:写入元素内容必须在更新
tail_之前完成(即 store element → store tail),否则消费者可能读到未初始化内存 - 示例节选:
bool push(const T& data) { size_t tail = tail_.load(std::memory_order_acquire); size_t next_tail = (tail + 1) & (CAPACITY - 1); if (next_tail == head_.load(std::memory_order_acquire)) return false; buffer_[tail] = data; tail_.store(next_tail, std::memory_order_release); return true; }
别碰多生产者多消费者(MPMC)的纯手写无锁链表队列
MPMC 链表队列需要解决三个硬问题:ABA(靠 tagged pointer + CAS2 或 double-word CAS)、内存回收(Hazard Pointer / Epoch-Based Reclamation)、以及节点分配(malloc 本身可能锁)——这些机制任意一个实现不到位,都会导致 crash 或泄漏。
即使参考经典 Michael-Scott 算法,其原始论文实现也假设指针+tag 可原子读写(即 uint128_t),而 x86-64 仅支持 cmpxchg16b(需 CPU 支持且编译器生成可靠汇编),ARMv8.1+ 才有 LDAPR 等等价指令。GCC/Clang 对 __int128 原子操作的支持也不统一。
- 真实项目中,95% 的所谓“自研无锁队列”最终都 fallback 到带锁或被替换成
boost::lockfree或moodycamel::ConcurrentQueue -
moodycamel::ConcurrentQueue是更现代的选择:MPMC、无锁、支持动态扩容、内存友好,但它的“无锁”指 enqueue/dequeue 不互斥,内部仍有少量轻量同步(如 ticket lock for producer token)——这比强行追求 100% CAS 更务实 - 如果真要研究原理,建议从
std::atomic<uintptr_t></uintptr_t>实现 tagged pointer 入手,而不是一上来就写完整队列
无锁的核心不是“不用锁”,而是“避免线程因同步原语阻塞”。很多场景下,一次 std::mutex 加锁的开销远低于调试一个内存序错误导致的偶发崩溃所花的时间。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











