无锁队列moodycamel::ConcurrentQueue

概述

moodycamel::ConcurrentQueue是由加拿大程序员 Cameron Desrochers 开发的一个高性能、多生产者、多消费者的无锁队列。它以其卓越的性能和丰富的功能而闻名,被广泛应用于游戏开发、金融交易、服务器等对性能要求极高的领域。如果你的应用场景中存在大量的生产者-消费者竞争,并且对性能有极致要求,那么它绝对是一个值得深入研究和使用的优秀工具。对于大多数常规的低并发场景,使用std::queue加锁或许就足够了。

核心特点:

  1. 无锁:使用原子操作而非互斥锁来实现线程同步,避免了线程阻塞、上下文切换和死锁问题。

  2. 多生产者/多消费者:完美支持多个线程同时入队和出队。

  3. 高性能:其设计精妙,即使在激烈竞争的情况下也能保持出色的吞吐量。

  4. 宽松的内存序:默认使用 C++11 内存模型中的memory_order_acquire和memory_order_release,在 x86/x64 架构上开销很小。

  5. 丰富的 API:提供了阻塞和非阻塞等多种操作接口。

核心设计与原理

它的高性能并非来自单一的“魔术”算法,而是多种技术结合的结果:

  1. 底层结构:它内部是由一个块(Block)的链表组成的。每个块是一个可以容纳多个元素的数组。这种批量分配的方式减少了内存分配和同步的开销。

  2. 细粒度锁(在生产者端):这是它最精妙的设计之一。虽然它对消费者是无锁的,但在生产者端,它使用了一种每生产者令牌(per-producer tokens)的机制。每个生产者线程在首次入队时,会隐式地或显式地获取一个“令牌”,这个令牌关联到内部的一个子队列。

    • 效果:大部分情况下,不同生产者线程操作的是不同的内部子队列,从而极大地减少了它们之间的竞争。只有当生产者需要分配新的块时,才会有一个全局的轻量级锁。

  3. 基于块的元素分配:

    • 生产者会预先分配一个块,然后在该块内顺序地填充元素。只有当块被填满后,才需要与其他生产者同步以获取新块。

    • 消费者从块的头部顺序消费元素。当一个块的所有元素都被消费后,该块会被回收,并在未来被复用。

  4. 显式批量操作:API 支持enqueue_bulk和try_dequeue_bulk,可以一次性入队或出队多个元素,这进一步分摊了同步开销,极大地提升了吞吐量。

基本用法

你需要从官方仓库获取代码: https://github.com/cameron314/concurrentqueue

无锁队列测试效果:http://moodycamel.com/blog/2014/a-fast-general-purpose-lock-free-queue-for-c++#benchmarks

1. 简单的入队和出队

cpp

#include <iostream> #include <thread> #include "concurrentqueue.h" // 包含头文件 moodycamel::ConcurrentQueue<int> queue; void producer() { for (int i = 0; i < 10; ++i) { queue.enqueue(i); // 入队操作 } } void consumer() { int item; for (int i = 0; i < 10; ++i) { // try_dequeue 是非阻塞的,成功取出返回 true while (!queue.try_dequeue(item)) { // 队列为空,可以忙等待、休眠或者做其他工作 std::this_thread::yield(); } std::cout << item << std::endl; } } int main() { std::thread prod(producer); std::thread cons(consumer); prod.join(); cons.join(); return 0; }
2. 使用令牌(Token)进行优化

如果你能预先知道生产者和消费者的数量,或者有长期存在的线程,使用ProducerToken和ConsumerToken可以获得最佳性能。

cpp

moodycamel::ConcurrentQueue<int> queue; void producer_with_token(moodycamel::ProducerToken& token) { for (int i = 0; i < 100; ++i) { queue.enqueue(token, i); // 使用令牌入队 } } void consumer_with_token(moodycamel::ConsumerToken& token) { int item; for (int i = 0; i < 100; ) { if (queue.try_dequeue(token, item)) { // 使用令牌出队 std::cout << item << std::endl; ++i; } else { std::this_thread::yield(); } } } int main() { // 为每个生产者和消费者预先创建令牌 moodycamel::ProducerToken prod_token(queue); moodycamel::ConsumerToken cons_token(queue); std::thread prod(producer_with_token, std::ref(prod_token)); std::thread cons(consumer_with_token, std::ref(cons_token)); prod.join(); cons.join(); return 0; }
3. 批量操作

这是性能最高的使用方式。

cpp

void bulk_producer(moodycamel::ProducerToken& token) { int items[100]; for (int i = 0; i < 100; ++i) { items[i] = i; } // 一次性入队100个元素 queue.enqueue_bulk(token, items, 100); } void bulk_consumer(moodycamel::ConsumerToken& token) { int items[50]; // 一次最多取出50个 size_t count; // 尝试批量出队,count 是实际取出的数量 while ((count = queue.try_dequeue_bulk(token, items, 50)) == 0) { std::this_thread::yield(); } for (size_t i = 0; i < count; ++i) { std::cout << items[i] << std::endl; } }
//批量读取最后所有数据以及释放 std::vector<int> temp; temp.reserve(m_dataQueue.size_approx()); // 预估大小,避免 vector 反复扩容 m_dataQueue.try_dequeue_bulk(std::back_inserter(temp), m_dataQueue.size_approx()); temp.clear();

与其他队列的对比

特性moodycamel::ConcurrentQueuestd::queue+ 互斥锁简单的无锁队列(如基于 Michael-Scott)
并发模型无锁,多生产者多消费者有锁无锁
性能极高,尤其在竞争激烈时差,锁竞争是瓶颈中等,CAS 操作可能成为瓶颈
设计复杂度高,内部使用块和令牌低中等
内存使用较高(预分配块)低低
适用场景高性能服务器、实时系统低并发、简单场景中等并发场景

优缺点总结

优点:

  • 极致性能:为高并发场景设计,吞吐量非常大。

  • 功能全面:支持阻塞/非阻塞操作、批量操作、令牌优化等。

  • 健壮性强:经过广泛测试,非常稳定。

  • 可移植性好:纯 C++11 实现,跨平台。

缺点:

  • 代码复杂:源代码非常复杂,理解和调试困难。

  • 内存占用:由于预分配块,可能会比简单队列占用更多内存。

  • API 稍显复杂:为了获得最佳性能,需要使用令牌和批量 API,增加了使用复杂度。