无锁队列之moodycamel::ConcurrentQueue
无锁队列之moodycamel::ConcurrentQueue
1 概述
moodycamel::ConcurrentQueue是加拿大程序Cameron Desrochers开发的高性能、多生产、多消费无锁队列。它以其卓越性能和丰富功能而闻名。被广泛用于游戏开发、金融交易、服务器等对性能要求极高的领域。如果在应用场景中存在大量生产-消费者竞争,并且对性能有极致要求,那么它绝对是一个值得深入研究和使用的优秀工具。
2 核心特点
- 无锁:使用原子操作而非互斥锁来实现线程同步,避免了线程阻塞、上下文切换和死锁问题。
- 多生产者/多消费者:完美支持多个线程同时入队和出队。
- 高性能:其设计精妙,即使在激烈竞争的情况下也能保持出色的吞吐量。
- 宽松的内存序:默认使用C++11内存模型中的memory_order_acquire和memory_order_release,在x86/x64架构上开销很小。
- 丰富的API:提供了阻塞和非阻塞等多种操作接口。
3 核心设计与原理
其高性能并非来自单一魔术算法,而是多种技术结合的结果:
- 底层结构:内部由一个块(Block)的链表组成。每个块是一个可以容纳多个元素的数组。这种批量分配内存的方式减少了内存分配和同步的开销。
- 细粒度锁(在生产者端):这是它最精妙的设计之一。虽然它对消费者是无锁的,但在生产者端,则使用了一种每生产者令牌(per-producer tokens)的机制。每个生产者线程在首次入队时,会隐式地或显示地获取一个“令牌”,这个令牌关联到内部的一个子队列。
效果:大部分情况下,不同生产者线程操作的时不同的内部子队列,从而极大地减少了它们之间的竞争。只有当生产者需要分配新的块时,才会有一个全局的轻量级锁。
- 基于块的元素分配:
- 生产者会预先分配一个块,然后在该块内顺序地填充元素。只有当块被填满后,才需要与其它生产者同步以获取新块。
- 消费者从块的头部顺序消费元素。当一个块的所有元素都被消费后,该块会被回收,并在未来被复用。
- 显示批量操作:API支持enqueue_bulk和try_dequeue_bulk,可以一次性入队或出队多个元素,这进一步分摊了同步开销,极大地提升了吞吐量。
4 基本用法
官方地址:
https://github.com/cameron314/concurrentqueue
4.1 简单入队和出队操作
#include <iostream> #include <thread> #include "concurrentqueue.h" // 包含头文件 moodycamel::ConcurrentQueue<int> queue; void producer() { for (int i = 0; i < 100000; ++i) { queue.enqueue(i); // 入队操作 std::cout << "in:" << i << std::endl; } } void consumer() { int item; for (int i = 0; i < 200000; ++i) { // try_dequeue 是非阻塞的,成功取出返回 true while (!queue.try_dequeue(item)) { // 队列为空,可以忙等待、休眠或者做其他工作 std::this_thread::yield(); } std::cout <<"got:" << item << std::endl; } } int main() { std::thread prod(producer); std::thread prod1(producer); std::thread prod2(producer); std::thread prod3(producer); std::thread cons(consumer); std::thread cons1(consumer); prod.join(); prod1.join(); prod2.join(); prod3.join(); cons.join(); cons1.join(); return 0; }
4.2 使用令牌(Token)进行优化
如果能预先知道生产者和消费者的数量,或者有长期存在的线程,使用ProducerToken和ConsumerToken可以获得最佳性能。
#include <iostream> #include <thread> #include ".concurrentqueue.h" // 包含头文件 moodycamel::ConcurrentQueue<int> queue; void producer_with_token(moodycamel::ProducerToken& token) { for (int i = 0; i < 100000; ++i) { queue.enqueue(token, i); // 使用令牌入队 } } void consumer_with_token(moodycamel::ConsumerToken& token) { int item; for (int i = 0; i < 100000; ) { 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; }
4.3 批量操作
这是性能最高的使用方式。
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; } }
5 与其它队列的对比
| 特性 | moodycamel::ConcurrentQueue | std::queue+互斥锁 | 简单的无锁队列(如基于Michael-Scott) |
| 并发模型 | 无锁,多生产者多消费者 | 有锁 | 无锁 |
| 性能 | 极高,尤其在竞争激烈时 | 差,锁竞争是瓶颈 | 中等,CAS操作可能成为瓶颈 |
| 设计复杂度 | 高,内部使用块和令牌 | 低 | 中等 |
| 内存使用 | 较高(预防分配块) | 低 | 低 |
| 使用场景 | 高性能服务器、实时系统 | 低并发、简单场景 | 中等并发场景 |
6 优缺点总结
6.1 优点
- 极致性能:为高并发场景设计,吞吐量非常大。
- 功能全面:支持阻塞/非阻塞操作、批量操作、令牌优化等。
- 健壮性强:经过广泛测试,非常稳定。
- 可移植性好:纯C++11实现,跨平台。
6.2 缺点
- 代码复杂:源码非常复杂,理解和调试困难。
- 内存占用:由于预分配块,可能会比简单队列占用更多内存。
- API稍显复杂:为了获得最佳性能,需要使用令牌和批量API,增加了使用复杂度。
7 测试效果


浙公网安备 33010602011771号