C++11多线程——单生产者多消费者模型

背景简介

单生产者多消费者模型适用于生产数据无需占用太多CPU,而消费数据需要大量CPU运算并且计算任务没有冲突的场景,将消费者计算任务分配到多核中加速运算。

示例目的

模拟实际多线程中单生产者多消费者,实现多线程模型,充分利用多核CPU特性进行并行计算。

设计要点

  1. 一个生产者产生一个数字,多个消费者处理这个数字(将其值加到公共的Sum中)。
  2. 消费者处理需要模拟一定的延时。
  3. 消费者获得生产者生产的数据,需要对数据加锁,防止其他消费者同时计算。
  4. 消费者取走数据后要通知生产者生产新的数据。
  5. 目前实现的是长度为1的队列,存储在CurrValue中,即生产者产出数据只有1个。当值为0时表示队列空,生产者可以工作;当值不为0时表示数据内容,消费者可以取出数据,消费者在取出数据后,需要将其置为0表示数据已取出。
  6. 代码使用GTest框架运行,可以将TEST(CppThreadTest, ConditionVariable)改成main函数并去除相应的校验直接执行。

代码示例(含注释)

代码实现过程以及每行的详细注释。

内部机制解析

  1. 为什么要条件变量?使用其他方法可以吗?
    这里使用条件变量检测条件并不是必须的,主要是为了解决以下方法的缺陷:

    1. 在消费者线程中通过while的方式检测,这样会导致CPU忙检测。
    2. 在消费这线程中通过while的方式检测,在数据未准备好时,通过usleep延时一段时间,比如100ms,虽然解决了CPU忙的问题,但是这样会导致部分数据处理时间过长,延时过大。
  2. 条件变量调用的wait内部做了什么?
    调用条件变量的wait的线程需要先获得lock,内部会对锁进行unlock使得其他并行线程也进入到wait中,然后进入阻塞状态,直到收到notify通知,wait返回的线程会获得锁,但是并不一定会达到可用的条件,比如上例中消费者在处理完数据解锁后,锁被其他消费者抢走,此时生产者并没有生产好数据,此处就需要条件变量的第二个参数来判断条件是否成立,如果不成立,则重新回到wait等待中,在打开代码中所有流程输出语句后,可以看到下面的情况。
    生产19997———–
    线程15抢到锁
    线程15即将wait,释放锁
    线程15wait后,value=19997
    线程15通知生产者
    线程15释放锁,处理19997
    生产19998———–
    线程16抢到锁
    线程16即将wait,释放锁
    线程16wait后,value=19998
    线程16通知生产者
    线程16释放锁,处理19998
    线程0抢到锁
    线程0即将wait,释放锁
    线程8抢到锁
    线程8即将wait,释放锁
    生产19999———–
    线程0wait后,value=19999
    线程0通知生产者
    线程0释放锁,处理19999
    解析一下:

    1. 在线程16释放锁后,通知生产者,生产者并没有被唤醒,而被线程0抢到了锁。
    2. 线程0抢到锁之后,进行wait,wait对锁进行unlock,被线程8抢到锁,线程8进入wait状态后,继续释放锁,最终被生产者线程抢到锁,继续生产。
  3. wait还有一个重载可以不用传入第二个判断条件成立的参数,但是此时需要在wait外部进行判断:
  4. 在生产者最后完成所有生产任务,置Stop为true后,需要调用GetCV.notify_all();通知所有的消费者线程,不然可能会有部分消费者线程阻塞在wait中永远出不来。在消费者调用wait,拿到锁后,需要判断退出条件,因为最后生产者置退出标志位后通知所有线程,此时数据并未准备好,但是Stop标志位已经表示可以退出,此时消费者线程需要做退出操作。

待解疑问

  1. 目前GetCV和SetCV使用同一个Mutex,是否需要分别使用?
    楼主尝试过使用不同的Mutex,但是会被锁住。
  2. notify和unlock的调用顺序问题,谁先谁后?有什么区别?
    维基百科条件变量词条中描述如下,目前没有一个示例能看出区别,需要补充:

    即将离开临界区的线程是先释放互斥锁还是先notify操作解除在条件变量上挂起线程的阻塞?表面看两种顺序都可以。但一般建议是先notify操作,后对互斥锁解锁。因为这既有利于上述的公平性,同时还避免了相反顺序时可能的优先级倒置。这种先notify后解锁的做法是悲观的(pessimization),因为被通知(notified)线程将立即被阻塞,等待通知(notifying)线程释放互斥锁。很多实现(特别是pthreads的很多实现)为了避免这种“匆忙与等待”(hurry up and wait)情形,把在条件变量的线程队列上处于等待的被通知线程直接移到互斥锁的线程队列上,而不唤醒这些线程。

参考资料

anyShare分享到:

原文地址:http://godmoon.wicp.net/blog/index.php/post_79.html,转载请注明出处

Moon发表于2015年12月1日
打赏作者

您的支持将鼓励我们继续创作!

[微信] 扫描二维码打赏

[支付宝] 扫描二维码打赏

发布者

sytzz

学会用简单的语言将复杂的问题说清楚。

《C++11多线程——单生产者多消费者模型》有1个想法

发表评论

电子邮件地址不会被公开。