回来填坑了,希望能帮到其他人
项目中一共有三个文件
- blockingconcurrentqueue.h: 阻塞并发队列.代码量 500行
- concurrentqueue.h: 并发无锁队列.主要文件,代码量 3000行
- lightweightsemaphore.h: 轻量级信号量.用于阻塞并发队列使用. 代码量 500行
只要把concurrentqueue.h这个文件啃明白,剩下两个文件都是小菜一碟.
这个项目大量使用了c++11的原子操作,所以需要了解内存模型,以及std::memory_order_xxx这些的含义,还有掌握一丢丢模板技术
这个项目因为都将源码写入一个头文件中,对于我这种带有一点点强迫症的人,还有主要就是看代码太难读懂了.所以将代码拆分成多个文件,这样对理解代码更加有帮助.后续把拆分后的代码(并附上详细注释)上传到github,引用原项目链接.
拆分后的项目结构:

下面给出两个项目链接,喜欢该项目的同学希望能给原版作者一个star,写的非常不错.
原作者项目链接
我自己进行拆分后的项目链接
下面以我自己拆分后的项目讲解为主,因为原项目都在一个文件分析起来特别麻烦.
程序入口
#include "concurrent_queue_impl.h"
#include <iostream>
#include <thread>
using namespace std;
int main()
{
ConcurrentQueue<int> conqueue;
bool exit = false;
std::thread t1([&]{
for (int i = 0; i < 50; i++)
conqueue.Enqueue(i); // 插入元素
cout << "t1 insert done!\n";
});
std::thread t2([&]{
int ret = 0;
while (conqueue.TryDequeue(ret)) // 尝试弹出元素,如果返回值为true表示成功
cout << "t2: " << ret << "\n";
exit = true;
});
t1.join();
t2.join();
while (!exit);
return 0;
}
只需要引用concurrent_queue_impl.h就可以使用并发队列了.视角就从ConcurrentQueue类里面是怎么做,直接去看concurrent_queue_impl.h文件.
准备好,后面内容可能比较多,可能需要你花一段时间去消化,毕竟我自己分析也花费了大量时间.
ConcurrentQueue类分析
ConcurrentQueue类的定义在concurrent_queue.h中,然后实现在concurrent_queue_impl.h中
ConcurrentQueue类的定义:
class ConcurrentQueue
{
public:
// 构造、虚构函数
// 不支持拷贝和赋值函数,设置为delete
public:
// 入队操作
bool Enqueue(T const& item)
template <typename It>
bool EnqueueBulk(It item_first, size_t count); // 批量插入
bool TryEnqueue(T const& item); // 尝试入队,有可能插入失败
template <typename It>
bool TryEnqueueBulk(It item_first, size_t count); // 尝试批量入队
// ... 省略一些入队的其他版本函数
// 出队操作
template <typename U>
bool TryDequeue(U& item); // 尝试出队
template <typename It>
size_t TryDequeueBulk(It item_first, size_t max); // 尝试批量出队
// ... 省略一些出队的其他版本函数
private:
// 成员变量
std::atomic<ProducerBase<T>*> producer_list_tail_; // 生产者列表尾部
std::atomic<std::uint32_t> producer_count_; // 生产者个数
std::atomic<size_t> initial_block_pool_index_; // 初始块池索引
Block<T>* initial_block_pool_; // 初始块池
size_t initial_block_pool_size_; // 初始块池大小
FreeList<Block<T>> free_list_; // 空闲链表
std::atomic<size_t> implicit_producer_hash_count_; // 隐式生产者哈希个数
std::atomic<ImplicitProducerHash<T>*> implicit_producer_hash_; // 隐式生产者哈希,和下面的指向同样的值
// 初始隐式生产者哈希,也就是下面的array
ImplicitProducerHash<T> initial_implicit_producer_hash_;
// 初始隐式生产者哈希条目,默认32大小(key、value形式)
std::array<ImplicitProducerKVP<T>, kInitalImplicitProducerHashSize>
initial_implicit_producer_hash_entries_;
// 用于标记隐式生产者哈希大小正在进行调整
std::atomic_flag implicit_producer_hash_resize_in_progress_;
std::atomic<uint32_t> next_explicit_consumer_id_; // 下一个显式消费者id
std::atomic<uint32_t> global_explicit_consumer_offset_; // 全局显式消费者偏移值
};
浏览上面的成员变量和成员函数,知道个大概.会发现,它整了implcit_produer和explicit_producer.,我称为隐式生产者,简写IP和显式生产者,简写EP,后面就直接用这俩称呼.
没错,入队和出队操作实际都是调用这俩的相应成员函数,实际值就保存在这俩类维护的内部数据结构. 也就是给用户提供了两种使用方式.
上面数据成员主要定义了块池和隐式生产者哈希表.
块池
initial_block_pool_主要是用来存储我们将入队的元素,它底层就是一个数组,通过索引存储具体的元素
隐式生产者哈希表
用于存储隐式生产者,入队和出队都是通过这个进行的.
构造函数
默认提供了两个构造函数
第一个构造函数
template <typename T>
ConcurrentQueue<T>::ConcurrentQueue(size_t capacity)
: producer_list_tail_(nullptr),
producer_count_(0),
initial_block_pool_index_(0),
next_explicit_consumer_id_(0),
global_explicit_consumer_offset_(0)
{
implicit_producer_hash_resize_in_progress_.clear(std::memory_order_relaxed);
PopulateInitialImplicitProducerHash(); // 初始化隐式哈希表
// (capacity & (kBlockSize - 1)) == 0 ? 0 : 1)
// 主要是获取capacity / kBlockSize如果有余数就多分配一块
PopulateInitialBlockList(capacity / kBlockSize +
((capacity & (kBlockSize - 1)) == 0 ? 0 : 1));
}
第9行是一个atomic_flag,用于多线程中分配隐式哈希表大小的标志.这一行将它设置为初始值
第10行初始化隐式哈希表
第11行初始化块池
看一下这两个函数是如何初始化的.
PopulateInitialImplicitProducerHash
template <typename T>
void ConcurrentQueue<T>::PopulateInitialImplicitProducerHash()
{
// kInitialImplicitProducerHashSize代表哈希表大小,默认32
if (kInitialImplicitProducerHashSize == 0)
return;
else
{
// 设置哈希表大小为0
implicit_producer_hash_count_.store(0, std::memory_order_relaxed);
auto hash = &initial_implicit_producer_hash_;
hash->capacity_ = kInitialImplicitProducerHashSize; // 设置哈希表的大小,默认32
// 指向ConcurrentQueue的成员std::array,默认为32大小
hash->entries_ = &initial_implicit_producer_hash_entries_[0];
for (size_t i = 0; i != kInitialImplicitProducerHashSize; i++)
// key_的含义是指向线程id,这里设置为初始值
initial_implicit_producer_hash_entries_[i].key_.store(kInvalidThreadId, std::memory_order_relaxed);
hash->prev_ = nullptr; // 哈希表不够也会创建新的,使用prev_和上一个哈希表连接起来
implicit_producer_hash_.store(hash, std::memory_order_relaxed);
}
}
隐式哈希表其实就是这个std::array,我们将哈希表的的一些成员指向它.
看上图,其中涉及到了两个数据结构,ImplicitProducerHash和ImplicitProducerKVP.
ImplicitProducerHash源码
template <typename T>
struct ImplicitProducerHash
{
ImplicitProducerHash()
: capacity_(0), entries_(nullptr), prev_(nullptr) { }
~ImplicitProducerHash() {}
size_t capacity_; // 哈希的容量
ImplicitProducerKVP<T>* entries_; // 条目
ImplicitProducerHash* prev_; // 指向上一次已分配的隐式生产者哈希
};
entries_就是指向std::array这个数组.我们在使用的时候都需要通过这个来进行访问哈希表
ImplicitProducerKVP源码
template <typename T>
struct ImplicitProducerKVP
{
ImplicitProducerKVP();
~ImplicitProducerKVP() {}
ImplicitProducerKVP(ImplicitProducerKVP&& other) noexcept;
ImplicitProducerKVP& operator=(ImplicitProducerKVP&& other) noexcept;
void Swap(ImplicitProducerKVP& other) noexcept;
std::atomic<ThreadId_t> key_; // 存储线程id的地址
ImplicitProducer<T>* value_; // 存储这个线程对应着的隐式生产者
};
一个隐式哈希表的底层项就是存储线程对应的id和这个线程对应的隐式生产者
PopulateInitialBlockList
template <typename T>
void ConcurrentQueue<T>::PopulateInitialBlockList(size_t block_count)
{
initial_block_pool_size_ = block_count; // 设置块池大小,默认也是32
if (initial_block_pool_size_ == 0)
{
initial_block_pool_ = nullptr;
return;
}
// 创建块池,大小为block_count
initial_block_pool_ = CreateArray<Block<T>>(block_count);
if (initial_block_pool_ == nullptr)
initial_block_pool_size_ = 0;
for (size_t i = 0; i < initial_block_pool_size_; i++)
initial_block_pool_[i].dynamically_allocated_ = false;
}
上面的代码很简单,主要就是分配一块指定字节的连续内存,用于存储块. 那么看一下Block数据结构的定义
Block
template <typename T>
struct Block
{
Block();
// ... 一些操作函数
// 下面是两个访问具体block的函数,可以通过下标进行访问
T* operator[](index_t index) noexcept;
T const* operator[](index_t index) const noexcept;
public:
Block* next_; // 指向下一个Block
std::atomic<size_t> elements_completely_dequeued_; // 用完空间的block个数. [隐式生产者会用到]
// 标志位数组,遍历查找是否有空闲的block(!!!效率较低) [显式生产者会用到]
std::atomic<bool> empty_flags_[kBlockSize <= kExplicitBlockEmptyCounterThreshold ? kBlockSize : 1];
std::atomic<std::uint32_t> free_list_refs_; //
std::atomic<Block<T>*> free_list_next_; // 指向下一个空闲的块链表
bool dynamically_allocated_; // 是否是动态分配
private:
// 存储队列中的元素, 元素类型为char,大小是 sizeof(T) * KBlockSize
alignas(alignof(T)) typename Identify<char[sizeof(T) * kBlockSize]>::type elements;
};
每一个Block内部都有一个数组,用于存储具体元素的值. 它的大小根据模板类型的大小*32(默认块大小).
前面说过,并发队列提供隐式生产者和显式生产者.所以,Block内部也定义了关于这两种方式的一些成员.
比如,对于隐式生产者,使用原子计数维护空闲还没有使用的空间.
对于显式生产者,使用原子标志位数组,通过遍历的方式查找哪一个空间还没用(效率较低些)
初始化后的块池和隐式生产者哈希表如下图所示
块池
隐式生产者哈希表
构造函数初始化了隐式生产者哈希表和块池,剩下就是该分析怎么入队和出队了.这个就涉及到了隐式生产者和显式生产者内部是入队和出队的.
留在下一篇,点击下面的链接跳转到下一篇文章
第二篇,隐式生产者的入队和出队源码分析