无锁队列之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)的机制。每个生产者线程在首次入队时,会隐式地或显示地获取一个“令牌”,这个令牌关联到内部的一个子队列。

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

  • 基于块的元素分配:
  1.  生产者会预先分配一个块,然后在该块内顺序地填充元素。只有当块被填满后,才需要与其它生产者同步以获取新块。
  2. 消费者从块的头部顺序消费元素。当一个块的所有元素都被消费后,该块会被回收,并在未来被复用。
  •  显示批量操作: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 测试效果

image

 image

 

posted @ 2025-12-13 19:02  钟齐峰  阅读(39)  评论(0)    收藏  举报