跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

C++ 实现基于原子变量的高性能无锁多生产者多消费者 (MPMC) 队列【源码】

因为MPMC场景下多个生产者/消费者竞争同一索引,裸用std::atomic增减头尾会导致ABA问题、内存重排及脏读;必须用版本号分离、CAS循环重试、fence同步与缓存行对齐才能保证安全。 为什么
std::atomic
不能直接拼出安全的 MPMC 队列 直接用
std::atomic
增减头尾索引,看似无锁,实际会因 ABA 问题和内存重排导致数据覆盖或读取脏值。比如两个生产者同时读到相同
tail
,都写入同一槽位;或者消费者读到更新后的
head
却没看到对应槽位已写入的数据。 真正可行的方案必须满足:(1)单次写入只被一个生产者完成;(2)读写之间有明确的发布-获取同步;(3)环形缓冲区索引更新不可回绕干扰。 避免裸用
std::atomic
管理索引 —— 改用
std::atomic
+ 位宽分离(低 k 位存索引,高比特存版本号),对抗 ABA 每次入队/出队必须用
compare_exchange_weak
循环重试,而非简单
fetch_add
写入元素后必须用
std::atomic_thread_fence(std::memory_order_release)
,读取前用
std::atomic_thread_fence(std::memory_order_acquire)
boost::lockfree::queue
能直接用吗?什么场景下会翻车 能用,但默认构造是固定容量、不支持动态扩容,且底层使用
std::atomic
索引 —— 在高吞吐(>10M ops/s)、长时间运行(数小时)的场景下,32 位计数器可能溢出回绕,触发未定义行为。 它也不暴露底层缓冲区指针,无法做零拷贝投递或自定义内存布局(如对象 placement-new 构造)。 立即学习 “ C++免费学习笔记(深入) ”; C函数速查手册(CHM版) C函数速查手册(CHM版) 下载 若队列生命周期短、容量预估准确、吞吐中等(boost::lockfree::queue 若需支持 move-only 类型(如
std::unique_ptr
),必须确保
T
满足
is_nothrow_move_constructible
,否则异常路径会破坏原子性 调试时遇到
segmentation fault
且堆栈指向
boost::lockfree::detail::freelist_stack
,大概率是对象析构时队列仍在被并发访问 手写环形缓冲区时,
capacity
必须是 2 的幂吗 不是必须,但强烈建议。非 2 幂容量会导致模运算
index % capacity
编译为除法指令,在 x86 上延迟高达 20+ 周期;而
index & (capacity - 1)
是单周期位运算。 更重要的是,2 幂容量配合“头尾差值用有符号比较”可避免无符号整数溢出判断的复杂分支 —— 例如用
int64_t tail_seq - int64_t head_seq > 0
判断是否有数据,比
(tail - head) % capacity != 0
更快更稳。 分配缓冲区内存时用
aligned_alloc(64, capacity * sizeof(T))
,避免 false sharing(尤其在多核间频繁切换 head/tail 时) 每个槽位额外加一个
std::atomic
作为 sequence 标记,初始为 0;生产者写入前设为
tail_seq - 1
,消费者读取后设为
tail_seq + capacity
不要复用
std::vector
—— 它的内存可能不满足缓存行对齐,且
resize()
会破坏已发布的 sequence 值 怎么验证你的 MPMC 队列真没锁、也没数据竞争 靠测压跑通不等于正确。要用
ThreadSanitizer
(Clang/GCC)编译并运行压力测试,它会报告所有 data race 和 unlock-of-unlocked-mutex 类错误。注意:TSan 对
std::atomic
的检查依赖正确的 memory order 标注 —— 若漏写
memory_order_acquire
,它会报出假阳性“concurrent write”。 测试用例必须包含:2 生产者 + 2 消费者持续 push/pop 100 万次,中间穿插随机 sleep(模拟调度抖动) 用
perf record -e cycles,instructions,cache-misses
对比 lock-based 队列,MPMC 的 cache-misses 应显著更低( 如果 valgrind + DRD 报 “possible data race on address”,优先检查是否忘了对非原子成员变量(如统计计数器)加
std::atomic
或锁 最常被忽略的是:消费端在读取元素后,没调用
T
的析构函数(若
T
有非平凡析构),导致内存泄漏或后续 placement-new 失败。这不会触发 TSan,但会让程序在长时间运行后崩在 operator new。

相关文章