并发队列(concurrentqueue)源码详细剖析 [第四篇 - implicit producer出队和批量入队]

0 阅读3分钟

第三篇链接

这一篇分析出队和批量出队操作. 上一篇讲解的很粗糙,但是把主要逻辑讲出来了,大家知道他做了什么就行了.
最后写一个总结,分析一下这个并发队列高效的地方到底在哪.但是并不代表完结,因为这个开源项目还提供了显式生产者.但是这个需要过一段时间才能分享出来了.

测试代码

int main()
{
    ConcurrentQueue<int> conqueue;

    std::thread t1([&]{
        vector<int> vec{1, 2, 3, 4, 5};
        conqueue.EnqueueBulk(vec.begin(), vec.size());
    });
    

    std::thread t2([&]{
        vector<int> results;
        int tmp = 0;
        while (conqueue.TryDequeue(tmp))
            results.push_back(tmp);
    });
    
    t1.detach();
    t2.detach();
    while (true);

    return 0;
}

上面代码定义了两个线程,用于模仿真实生产者-消费者模型.生产者和消费者不是同一个线程.来看它是怎么处理的.
对于出队的函数,都是TryXXX这样的函数.返回true就代表元素.
咱们从ConcurrentQueue类里面TryDequeue函数是怎么写的

ConcurrentQueue的TryDequeue源码剖析

image.png 上面代码,从producer_list_tail_遍历之前创建的生产者.还记得第一篇中入队操作都需要创建生产者吗?
下面给出一个调用链
image.png

回到TryDequeue函数里,一共遍历3次,找出元素个数最多的那个生产者.大家不要看到就糊涂了,是这一次出队我们选择元素最多的那个生产者. 我们通过while循环会再次调用这个函数的.

然后调用生产者的Dequeue函数,获取元素出队. 如果获取元素失败的话,那么找一个生产者尝试出队.
Ok,这个函数到这里结束,让我们把视角又回到生产者Dequeue函数

隐式生产者的Dequeue出队

在分析这个函数之前,先介绍两个成员变量.这俩我觉得挺有意思的,学到了

image.png 在隐式生产者的基类ProducerBase定义了两个成员变量,分别是乐观出队计数器和过度提交计数器
作者在这里引入这俩主要就是优化多线程环境下的出队操作,减少锁竞争带来的开销.
大家以后也可以尝试在自己的代码加入这种机制.

image.png image.png 首先根据乐观计数器-过度提交计数器是否大于tail_index_.如果大于那就说明真没数据了,直接返回false.
如果小于的情况,将乐观计数器加1,再次获取tail_index,这都是没有用锁的.
再次判断是否小于tail_index,用了两次验证提高真的有数据的准确性.然后将head_index_加1,指向下一个要读取的内容下标. 获取对应的块索引条目项,从块中获取之前入队的数据. 调用移动构造将数据移动到参数中. 期间还会针对移动构造出现异常的情况. 最后设置块的空块数量,如果这个块已经没有任何数据存储了,那么直接加入空闲链表中(调用ConncurrentQueue类的函数).
在这里,我觉得没有验证获取出来得数据正确性.上面两次乐观计数器判断成功了,但是实际上真的没有数据可读了,那么获取出来的就是一个假的,上面代码没有验证最后向用户返回true,但其实是一个假的数据,这肯定会造成业务逻辑上的bug

image.png 就在这里没有验证数据是真的还是假的.

隐式生产者的DequeueBulk批量出队

未完待续...
等待更新,先处理最近手头事情