高效C++无锁(lock free)队列moodycamel::ConcurrentQueue

moodycamel::ConcurrentQueue一个用C++11实现的多生产者、多消费者无锁队列。

github地址:https://github.com/cameron314/concurrentqueue
它具有以下特点:
1.快的让人大吃一惊,详见不同无锁队列之间的压测对比
2.单头文件实现,很容易集成到你的项目中
3.完全线程安全的无锁队列,支持任意线程数的并发
4.用C++11实现,尽可能move元素,而不是copy元素
5.模板化,无需专门处理指针,内部自动管理内存
6.对元素类型或最大计数没有人为限制
7.内存可以预先一次分配,也可以根据需要动态分配
8.跨平台,所有操作都通过标准C++11原语完成
9.支持超快批量操作
10.包括低开销阻塞版本(BlockingConcurrentQueue)
11.异常安全
现实中,很少见到用C++实现的完全无锁队列。Boost有一个——boost::lockfree::queue,但它仅限于具有简单赋值运算符和简单析构函数的对象。Intel的TBB(tbb::concurrent_queue)队列不是无锁的,也需要简单的构造函数。有许多学术论文声称用C++实现了无锁队列,但很难找到可用的源代码,对源码的测试就更难找到了。
这个队列不仅比其他队列(在大多数情况下)具有更少的限制,而且速度更快。它经过了良好的测试,并提供了诸如批量入队/出队等高级功能。
简而言之,在C++开源世界中有一个关于无锁队列的空白,我用我所能做到的最快、最完整、测试良好的实现来填补这个空白。这个实现就是moodycamel::ConcurrentQueue

一.设计

为了获得更好的性能,内部元素存储使用的是连续内存块而不是链表。每个生产者都维持自己的专属queue,而不同的消费者会以不同的顺序去访问各个生产者的queue,直到遇到一个不为空的queue。也就是说,这里的的MPMC(multiple producer multiple consumer)的队列建立在了SPMC(single producer multiple consumer)的多线程队列的基础上。这个SPMC的实现是lockfree的,同时还增加了批量操作。
首先就是在构建消费者的时候,尽可能的让消费者与生产者均衡绑定,内部实现是通过使用一个token来维持消费者与生产者之间的亲和性。其实最简单的亲和性分配的方法就是每个消费者分配一个生产者的编号,dequeue的时候采取轮询的方式,每次开始轮询的时候都以上次dequeue成功的生产者queue开始。
处理完了多生产者多消费者之间的映射,现在剩下的内容就是如何更高效的处理单生产者多消费者。moodycamel这里的主要改进就是单个queue的存储结构,这里采取的是两层的循环队列,第一层循环队列存储的是第二层循环队列的指针。一个队列只需要维护四个索引,考虑到原子性修改可以把消费者的两个索引合并为一个uint64t或者uint32t,因为只有消费者会发生数据竞争,为了方便比较,也顺便把生产者的两个索引合并为一个uint64t或者uint32t,这样就可以直接使用整数比较了。在enqueue的时候,数据复制完成之后,直接对生产者的索引自增即可。而dequeue的时候则没这么容易,此时首先自增消费者索引,然后判断当前消费者索引是否已经越过生产者索引,如果越过了,则对则对另外一个overcommit计数器进行自增,三个计数器合用才能获得真正的容量。
这里使用环形缓冲来扩容而不是采取列表来扩容,主要是因为连续的空间操作可以支持批量的enqueue和dequeue操作,直接预先占据一些索引就行了。

二.基本使用

整个队列的实现包含在一个头文件concurrentqueue.h中。阻塞版本位于另一个头文件blockingconcurrentqueue.h中,依赖concurrentqueue.h和lightweightsignal.h。队列的实现使用了某些关键的C++11特性,因此需要一个相对较新的编译器(例如VS2012+或g++4.8)。可以像使用任何其他模板化队列一样使用它。
一个简单的例子:

#include "concurrentqueue.h"

moodycamel::ConcurrentQueue<int> q;
q.enqueue(25);

int item;
bool found = q.try_dequeue(item);
assert(found && item == 25);

使用的函数:
ConcurrentQueue(size_t initialSizeEstimate),构造时指定元素个数
enqueue(T&& item) ,存一个元素,并分配相应的内存
try_enqueue(T&& item),存一个元素,但只有足够的内存已经分配时才能成功
try_dequeue(T& item),取一个元素,当有元素时true,当队列为空时返回false
需要注意的是队列对象要在被线程使用之前构造,在线程不再使用时销毁。
每个函数通常有两个版本,“显示”的版本接收用户分配的令牌(token),“隐式”的版本不使用令牌。使用显式函数几乎总是更快。
所有的API(伪代码)

# Allocates more memory if necessary
enqueue(item) : bool
enqueue(prod_token, item) : bool
enqueue_bulk(item_first, count) : bool
enqueue_bulk(prod_token, item_first, count) : bool

# Fails if not enough memory to enqueue
try_enqueue(item) : bool
try_enqueue(prod_token, item) : bool
try_enqueue_bulk(item_first, count) : bool
try_enqueue_bulk(prod_token, item_first, count) : bool

# Attempts to dequeue from the queue (never allocates)
try_dequeue(item&) : bool
try_dequeue(cons_token, item&) : bool
try_dequeue_bulk(item_first, max) : size_t
try_dequeue_bulk(cons_token, item_first, max) : size_t

# If you happen to know which producer you want to dequeue from
try_dequeue_from_producer(prod_token, item&) : bool
try_dequeue_bulk_from_producer(prod_token, item_first, max) : size_t

# A not-necessarily-accurate count of the total number of elements
size_approx() : size_t

阻塞版本:
如上所述,阻塞版本除了常规的接口外,还添加了wait_queue和wait_queue_bulk。阻塞版本开销非常低,但是速度略低于非阻塞版本。
这个版本可以指定超时(以微秒为单位或使用std::chrono对象)。
阻塞版本需要注意的是,当有线程在等待该队列时,你必须小心不要销毁它。哈哈,当然,非阻塞版本队列在使用时也不能被销毁。
一个阻塞队列的例子

#include "blockingconcurrentqueue.h"

moodycamel::BlockingConcurrentQueue<int> q;
std::thread producer([&]() {
    for (int i = 0; i != 100; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(i % 10));
        q.enqueue(i);
    }
});
std::thread consumer([&]() {
    for (int i = 0; i != 100; ++i) {
        int item;
        q.wait_dequeue(item);
        assert(item == i);
        
        if (q.wait_dequeue_timed(item, std::chrono::milliseconds(5))) {
            ++i;
            assert(item == i);
        }
    }
});
producer.join();
consumer.join();

assert(q.size_approx() == 0);

二.高级特性

1.令牌

为了加快操作速度,队列可以利用每个生产者和每个消费者的额外存储空间。此时需要令牌:您可以为每个线程或任务创建消费者令牌和/或生产者令牌(令牌本身不是线程安全的),并使用接受令牌作为其第一个参数的函数:

moodycamel::ConcurrentQueue<int> q;

moodycamel::ProducerToken ptok(q);
q.enqueue(ptok, 17);

moodycamel::ConsumerToken ctok(q);
int item;
q.try_dequeue(ctok, item);
assert(item == 17);

如果你刚好知道要消费哪个生产者(比如,在单个生产者、多消费者场景中),可以使用try_dequeue_from_producer方法,该方法接受生产者令牌而不是消费者令牌,从而减少一些开销。
当然,令牌也可用于阻塞版本。
当生产或消费许多元素时,最有效的方法是:
使用带有令牌的批量方法;
否则,请使用不带令牌的批量方法;
否则,请使用带有令牌的单项方法;
否则,请使用不带标记的单项方法;
话虽如此,但不要随意创建令牌——理想情况下是每个线程一个令牌。队列没有令牌时也能使用,但与令牌一起使用时表现最好。
注意,令牌实际上并没有绑定到任何给定的线程;技术上不要求它们是线程本身的,只要求它们每次只会被单个生产者/消费者使用。

2.批量操作

由于队列的新颖设计,让多个元素同时入队/出队与一次处理一个元素一样容易。这意味着可以大幅减少批量操作的开销。语法示例:

moodycamel::ConcurrentQueue<int> q;

int items[] = { 1, 2, 3, 4, 5 };
q.enqueue_bulk(items, 5);

int results[5];     // Could also be any iterator
size_t count = q.try_dequeue_bulk(results, 5);
for (size_t i = 0; i != count; ++i) {
    assert(results[i] == items[i]);
}

3.其他的特性

详见官方文档:https://github.com/cameron314/concurrentqueue

原文链接:https://blog.csdn.net/caoshangpa/article/details/78506322

  • 11
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草上爬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值