概述
moodycamel::ConcurrentQueue是由加拿大程序员 Cameron Desrochers 开发的一个高性能、多生产者、多消费者的无锁队列。它以其卓越的性能和丰富的功能而闻名,被广泛应用于游戏开发、金融交易、服务器等对性能要求极高的领域。如果你的应用场景中存在大量的生产者-消费者竞争,并且对性能有极致要求,那么它绝对是一个值得深入研究和使用的优秀工具。对于大多数常规的低并发场景,使用std::queue加锁或许就足够了。
核心特点:
无锁:使用原子操作而非互斥锁来实现线程同步,避免了线程阻塞、上下文切换和死锁问题。
多生产者/多消费者:完美支持多个线程同时入队和出队。
高性能:其设计精妙,即使在激烈竞争的情况下也能保持出色的吞吐量。
宽松的内存序:默认使用 C++11 内存模型中的
memory_order_acquire和memory_order_release,在 x86/x64 架构上开销很小。丰富的 API:提供了阻塞和非阻塞等多种操作接口。
核心设计与原理
它的高性能并非来自单一的“魔术”算法,而是多种技术结合的结果:
底层结构:它内部是由一个块(Block)的链表组成的。每个块是一个可以容纳多个元素的数组。这种批量分配的方式减少了内存分配和同步的开销。
细粒度锁(在生产者端):这是它最精妙的设计之一。虽然它对消费者是无锁的,但在生产者端,它使用了一种每生产者令牌(per-producer tokens)的机制。每个生产者线程在首次入队时,会隐式地或显式地获取一个“令牌”,这个令牌关联到内部的一个子队列。
效果:大部分情况下,不同生产者线程操作的是不同的内部子队列,从而极大地减少了它们之间的竞争。只有当生产者需要分配新的块时,才会有一个全局的轻量级锁。
基于块的元素分配:
生产者会预先分配一个块,然后在该块内顺序地填充元素。只有当块被填满后,才需要与其他生产者同步以获取新块。
消费者从块的头部顺序消费元素。当一个块的所有元素都被消费后,该块会被回收,并在未来被复用。
显式批量操作: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::ConcurrentQueue | std::queue+ 互斥锁 | 简单的无锁队列(如基于 Michael-Scott) |
|---|---|---|---|
| 并发模型 | 无锁,多生产者多消费者 | 有锁 | 无锁 |
| 性能 | 极高,尤其在竞争激烈时 | 差,锁竞争是瓶颈 | 中等,CAS 操作可能成为瓶颈 |
| 设计复杂度 | 高,内部使用块和令牌 | 低 | 中等 |
| 内存使用 | 较高(预分配块) | 低 | 低 |
| 适用场景 | 高性能服务器、实时系统 | 低并发、简单场景 | 中等并发场景 |
优缺点总结
优点:
极致性能:为高并发场景设计,吞吐量非常大。
功能全面:支持阻塞/非阻塞操作、批量操作、令牌优化等。
健壮性强:经过广泛测试,非常稳定。
可移植性好:纯 C++11 实现,跨平台。
缺点:
代码复杂:源代码非常复杂,理解和调试困难。
内存占用:由于预分配块,可能会比简单队列占用更多内存。
API 稍显复杂:为了获得最佳性能,需要使用令牌和批量 API,增加了使用复杂度。