
C标准库未提供无锁队列因其设计目标为单线程安全与通用性而无锁队列涉及复杂并发协议内存序、ABA防护、内存回收、硬件依赖等。标准更侧重提供原子操作与同步原语由开发者基于场景选择实现。工业级方案如moodycamel、TBB已成熟推荐根据生产者/消费者拓扑选用SPSC、MPSC或分片队列避免盲目手写高难度的MPMC无锁队列。一、先澄清概念无锁 ≠ 无同步“无锁lock-free”在 C 里的严格含义是系统中总有至少一个线程在有限步内取得进展没有线程能通过持锁把别人永久堵死。它不等于不用原子不自旋不竞争一定比加锁快所以“无锁队列”不是把std::mutex删掉就行而是把正确性建立在std::atomicCAS / fetch-addacquire / release / seq_cst内存回收策略hazard pointer / epoch / 节点池ABA 防护这几件事上。二、为什么 C 标准库不提供“无锁队列”1.std::queue的定位是容器适配器不是并发原语std::queue默认包std::deque设计目标是单线程语义清晰异常安全泛型、可替换底层容器不偷偷引入同步开销如果给它加内部锁所有单线程用户都在为并发买单如果给它加无锁实现又要绑定固定容量、节点分配、内存序、回收策略。2. 并发队列的“语义空间”太大一个并发队列至少要回答维度选项生产者1 / N消费者1 / N容量有界 / 无界阻塞阻塞 / 非阻塞 / 自旋顺序每生产者保序 / 全序 / 无全序异常元素构造抛错怎么办回收hazard pointer / RCU / 节点池进度保证lock-free / wait-free / obstruction-freeWG21 的并发队列提案P0260讨论了很多年最后把具体 lock-free 队列推迟了因为“一个队列适配所有场景”在标准里不现实。3. 无锁实现高度依赖硬件与 ABIx86 的 store-load 重排弱ARM/RISC-V 不是双字 CASDCAS不是所有平台都有缓存行、false sharing、NUMA 会影响实现节点什么时候能 free是安全性问题不是小细节标准库如果提供一个“表面上可移植”的 MPMC 无锁队列底层要么退化成锁要么在某些平台并不真无锁。4. 标准委员会更想把“执行模型”做对最近十年的重点不是“再给一个并发容器”而是std::thread/jthreadatomic/memory_orderlatch/barrier/semaphoreexecutors / sender-receiverP2300 系更上层的任务图也就是说标准提供原子和内存模型让你造并发队列而不是替你拍板用哪种并发队列。5. 生产级实现已经在外面长好了boost::lockfree::queue/spsc_queuemoodycamel::ConcurrentQueueatomic_queueIntel TBB / oneTBBconcurrent_queueFollyMPMCQueue这些库里藏着十年级优化缓存行填充、批量入队、token、隐式 producer、内存预热、动态扩容策略。标准库一开始很难超过它们。三、无锁队列的四种主流形态1. SPSC 环形缓冲最稳最推荐手写一个生产者、一个消费者两个原子索引head_/tail_不需要 CAS适合音频、网络收发包、日志、渲染命令2. MPSC多生产者单消费者生产者之间用 CAS 抢“写位置”消费者只读不需要 CAS适合“多线程产任务单线程消费执行”3. MPMC 链表队列Michael-Scott头尾指针都用 CAS 推进节点动态分配ABA、回收、内存序全是坑学术界经典工程上常被 ring-buffer 变体取代4. MPMC 分片 / token 队列每个生产者一个局部队列消费者轮询分片moodycamel 的核心思路高吞吐但实现复杂四、先给一个“真能跑”的 SPSC 无锁队列下面这个是工业里最常用的一类固定容量、单生产者单消费者、无锁、无动态分配、移动语义安全。#include atomic #include vector #include utility #include new template typename T class SPSCLockFreeQueue { public: explicit SPSCLockFreeQueue(std::size_t capacity) : capacity_(capacity 1) // 留一个空槽区分满/空 , buf_(new T[capacity_]) , head_(0) , tail_(0) {} ~SPSCLockFreeQueue() { while (pop([](T){})) { /* 析构剩余元素 */ } delete[] buf_; } // 生产者调用 template typename U bool push(U item) { std::size_t tail tail_.load(std::memory_order_relaxed); std::size_t next (tail 1) % capacity_; if (next head_.load(std::memory_order_acquire)) { return false; // 满 } buf_[tail] std::forwardU(item); tail_.store(next, std::memory_order_release); return true; } // 消费者调用消费成功执行 consumer否则返回 false template typename F bool pop(F consumer) { std::size_t head head_.load(std::memory_order_relaxed); if (head tail_.load(std::memory_order_acquire)) { return false; // 空 } std::invoke(std::forwardF(consumer), buf_[head]); buf_[head].~T(); // 如果 T 有非平凡析构 head_.store((head 1) % capacity_, std::memory_order_release); return true; } bool empty() const { return head_.load(std::memory_order_acquire) tail_.load(std::memory_order_acquire); } bool full() const { std::size_t tail tail_.load(std::memory_order_acquire); std::size_t next (tail 1) % capacity_; return next head_.load(std::memory_order_acquire); } private: SPSCLockFreeQueue(const SPSCLockFreeQueue) delete; SPSCLockFreeQueue operator(const SPSCLockFreeQueue) delete; std::size_t capacity_; T* buf_; alignas(std::hardware_destructive_interference_size) std::atomicstd::size_t head_; alignas(std::hardware_destructive_interference_size) std::atomicstd::size_t tail_; };内存序为什么这样写生产者写buf_[tail]后tail.store(release)保证数据写对其他线程可见消费者head.load(acquire)/tail.load(acquire)保证看到生产者写入的数据单生产者单消费者下head和tail分别只被一方修改所以不需要 CASalignas(hardware_destructive_interference_size)是为了防 false sharing不是装饰。五、最小测试程序#include iostream #include thread int main() { SPSCLockFreeQueueint q(1024); std::thread producer([] { for (int i 0; i 100000; i) { while (!q.push(i)) { std::this_thread::yield(); } } }); std::thread consumer([] { long long sum 0; int count 0; int val 0; while (count 100000) { if (q.pop([](int v) { val v; })) { sum val; count; } else { std::this_thread::yield(); } } std::cout sum sum \n; }); producer.join(); consumer.join(); return 0; }预期sum4999950000如果你看到数据重复、丢值、崩溃通常不是“无锁慢”而是多生产者用了 SPSC 队列内存序写错消费者在pop后又用了已被析构的对象把head/tail当普通 int 用六、为什么“MPMC 无锁队列”别轻易手写真正难的不是入队出队算法而是这三件事1. ABA 问题指针从 A 变 B 又变回 ACAS 以为没变其实中间状态已经废了。解法tagged pointer、版本号、双字 CAS。2. 内存回收节点被一个线程弹出另一个线程还在 CAS 它。你不能随便delete。解法hazard pointersepoch-based reclamation节点池 / 固定分片quiescent-state / RCU3. 进度与饥饿无锁只保证“系统有进展”不保证某个线程不饿死延迟有上界高争用时比锁快事实上低争用下无锁自旋可能比std::mutex还慢。七、工程选型建议场景建议单生产者单消费者SPSC ring buffer自己写也行多生产者单消费者MPSC ring / moodycamel ReaderWriterQueue多生产者多消费者moodycamel::ConcurrentQueue / TBB / Folly需要阻塞等待mutex condvar或sem 队列任务队列 / 线程池有界队列 条件变量通常够用高频交易 / 网卡轮询SPSC 亲和 大页 无系统调用别一上来就 MPMC 无锁队列。很多“线程池卡顿”问题换成 SPSC 管道 单消费者执行器就解决了。八、一句话总结C 标准库不提供无锁队列不是因为它“落后”而是因为无锁队列不是单一数据结构而是一族高度依赖场景、硬件和内存模型的并发协议。标准给你原子、内存序、线程和 barrierBoost/ moodycamel/ TBB给你经过十年打磨的队列你该做的是按生产者/消费者拓扑选型而不是追“无锁”两个字。