生产者消费者模型
举例理解其特点
举个例子:
我们现实生活中:工厂,超市,人[消费者]之间的关系就是一个典型的生产者消费者模型
这个模型的基本工作流程是:
- 工厂制造商品供货给超市,消费者到超市购买商品
现实生活中,为什么会优化出这样的一种模型?
为什么消费者不直接去工厂买东西?
主要有以下3个原因:
- ①为了效率:
-
1.对于工厂来说,消费者如果直接来买商品,一个消费者一次购买的商品数量非常有限,而且一个工厂的产品种类一般并不多,所以消费者还得去不同工厂买东西,工厂为了防止卖不完,只能减少商品的生产,即
降低了工厂的生产效率
但是如果卖给超市,就可以一次性卖一大车货物,多找几个超市,就可以做到:生产多少卖多少了 -
2.对于消费者,工厂的占地面积比较大,所以一般都距离居民地比较远,而超市一般都比工厂占地面积小的多,离消费者很近
所以消费者去超市买东西比去工厂更快
而且一个工厂的产品种类一般并不多,但是超市售卖的产品种类繁多
-
- ②有了超市就可以做到生产者和消费者之间的解藕
-
1.一个工厂关门了,超市换一个工厂进货就行了,只要进的货还是一样的,就丝毫不影响消费者消费
事实就是如此,我们消费者从超市买东西时,根本就不知道这个商品是哪个工厂生产的,我们也不关心
消费者只管购买商品就行了 -
2.到超市消费者是谁,是什么群体,工厂也不知道,也不关心,这个是超市该关心的
因为工厂只把商品卖给超市,超市卖给谁和工厂无关,工厂只管生产就行了 -
3.有了超市的存在
左侧的工厂发生变化不影响右侧的消费者
右侧的消费者发生变化不影响左侧的工厂
这不就解藕了吗?
-
- ③支持忙闲不均
有了超市这个缓冲区的存在-
1.在购物潮之前,超市就可以通知工厂多生产一些商品,超市多进货,方便应对更多消费者
-
2.在囤积的商品多的时候,超市也可以搞活动吸引消费者,并且通知工厂生产地慢一点
-
超市本质上就是工厂和消费者之间的缓存
即:超市支持工厂“预加载”产品,消费者可以一定程度“预定”产品
上述例子对应到计算机中的生产者消费者模型就是:
- ①工厂:生产者线程
- ②消费者:消费者线程
- ③超市:以某种数据结构组织的内存区域
- ④商品:数据
和线程安全再对应一下:
- ①超市:
共享/临界资源
- ②我们要研究生产者消费者模型,就需要研究清楚多个生产者和多个消费者之间的同步互斥关系!
一共有3种关系:-
1.
生产者和生产者之间:互斥
因为如果多个生产者同时向超市(共享资源)写数据,可能会产生线程安全问题
因为同时有多个线程对同一份共享资源进行修改,很容易互相覆盖,出现并发线程安全问题
即:线程之间切换时可能导致数据不一致问题
其次共享资源的空间就那么大,多个生产者线程同时去写的话,一个线程写的多了,另一个线程就只能少写一点了
或者线程a刚向共享资源中的一个位置写了一个1,线程b就跑过来把那个位置的1改成10了
所以生产者线程之间是竞争关系 -
2.
消费者和消费者之间:互斥
消费者线程,虽然是进入临界资源读取数据,但是其实也会对共享资源进行修改
即:线程拿走了一个数据a,其他线程看待这个这个数据a就是过期数据了(和到超市买东西一样,买了一包方便面,超市就少了一包方便面)
既然会修改,那么多个消费者线程同时进入共享资源进行修改的话,也可能会出现并发切换导致的线程安全问题
比如:线程a和线程b同时访问共享资源,线程a先把数据X消费走了,但是因为是同时进入线程b任认为数据X还是有效的,也拿了数据X(如果共享资源是数组,那可以把数据X理解为数组中的一个元素)
就会导致一份数据,被使用了两次 -
3.
生产者和消费者之间:互斥+同步
因为不管是生产者线程还是消费者线程,访问共享资源时,都会进行修改,多线程并发修改共享资源,就可能会出现并发线程安全问题
所以它们首先得是互斥的
但是如果只有互斥,那么消费者如果不知道有没有数据,就只能不断地去轮询检测,每次轮询都要申请锁,那生产者线程就可能很难抢到锁,就一直生产不了数据,消费者线程就一直拿不到数据,造成恶性循环
就可能会导致锁的饥饿问题
所以需要同步关系来提高生产者和消费者模型的效率
即:
设置条件变量,让消费者线程在条件变量的等待队列中等,当生产者线程生产数据之后,才唤醒消费者线程,去读取数据
-
生产者消费者模型的阻塞队列版本
生产者消费者模型一般会使用一个阻塞队列来作为共享资源,进而实现多线程协作
生产者消费者模型的阻塞队列的特点:
①如果队列为空
,那么一个消费者线程如果来拿数据,它就会被阻塞
- ②如果队列
为满
,那么一个生产者线程如果还要向队列里写数据,它就会被阻塞 - ③如果队列不空也不满,那么生产者就可以向队列尾部写数据,消费者就可以向队列头部拿数据
阻塞队列类的简单实现
- ①
成员变量:
-
1.存储数据的容器
直接使用STL的queue
,因为数据的类型不确定,所以阻塞队列类是模板类 -
2.一把锁
阻塞队列自己会被所有线程看见,所以它是共享资源,所以需要锁来保护自己
要几把锁呢?
因为所有生产者线程之间,所有消费者线程之间,以及生产者和消费者之间
都是互斥的,所以它们得用同一把锁来实现互斥
-
3.生产者线程的条件变量
因为在满足一定条件(比如:阻塞队列满了,或者阻塞队列满了4/5了等)时,可以让所有生产者线程暂时暂停生产
(即去条件变量的等待队列中阻塞) -
4.消费者线程的条件变量
因为在满足一定条件(比如:阻塞队列为空,或者阻塞队列空了4/5了等)时,可以让所有消费者线程暂时暂停消费(即去条件变量的等待队列中阻塞)
为什么要搞两个条件变量?
一个条件变量虽然也可以实现生产者线程和消费者线程之间的同步
但是实现起来非常麻烦,而且不能区分条件变量的等待队列下的是生产者线程还是消费者线程
而且
两个条件变量可以很好地支持:
生产者消费者模型的第3个优点:忙闲不均 -
5.int cap:阻塞队列的最大容量
-
6.int cwait_num:在消费者条件变量的等待队列中等待的线程个数
-
7.int pwait_num:在生产者条件变量的等待队列中等待的线程个数
6和7成员变量的存在主要是为了方便实现线程之间的互相唤醒机制
(即生产者线程生产了之后,可以唤醒消费者线程来消费,反之同理)
-
- ②成员函数:
-
1.Equeue:生产数据
代码细节:
伪唤醒问题的解决
即:判断线程是否要进入条件变量的等待队列时,判断不能用if而要用while
不然就有可能出现伪唤醒问题:即在条件变量下等待的线程,唤醒条件其实并不满足
但是因为程序员编码的问题,可能意外被唤醒了
例如:
生产者消费者模型中,因为阻塞队列中没有数据,所以全部都5个消费者线程在条件变量的等待队列中等待
生产者线程生产了一个数据,意外地把唤醒了多个消费者线程
然后一个消费者线程抢到锁之后,把阻塞队列中那唯一的一个数据抢走了,它解锁之后
因为唤醒了多个消费者线程
所以锁可能又被一个消费者线程抢到了,但是此时阻塞队列中根本没有数据!
此时: -
1.如果此时是使用if进行“线程是否需要进入条件变量的等待队列"的判断的这个被伪唤醒的线程,重新申请并拿到锁之后,就直接"饿虎出笼"去肆意妄为了
-
2.如果是使用while进行“线程是否需要进入条件变量的等待队列”的判断的,这个被唤醒的线程,重新申请并拿到锁之后,也还是不能直接出循环,因为要再判断一下循环条件是否不满足了
虽然循环条件是"线程需要进入等待队列"的条件,但是如果这个条件满足,不就意味着线程不应该被唤醒吗?
-
-
2.Pop:获取并删除数据
-
3.IsEmpty:阻塞队列是否为空
-
4.IsFull:阻塞队列是否为满
生产者消费者模型的高效性是如果体现的?
生产者消费者模型的3种关系都包含互斥
即
所有线程访问阻塞队列的时候,都是通过同一把锁进行互斥访问的
那么每次访问阻塞队列的线程都只有一个,不管是取数据,放数据都是只能串行执行
即
任意时刻访问共享资源的时候,都最多只有一个线程能访问,所有线程访问共享资源是串行访问的
互斥访问,如何能够体现出多线程的高效性呢?
其实理解生产者消费者模型的高效性要从更高的视角来看
-
①生产者可以向阻塞队列生产数据,但是它的数据从哪里来?
生产者的数据一般从网络,外部硬件输入,其他线程等,但是这些一般都是需要一定条件(用户输入,网络传输等)才会出现数据给生产者
即:生产者要生产数据很可能需要阻塞等待!在阻塞等待期间,生产者肯定不能访问阻塞队列
-
②消费者可以从阻塞队列获取数据,但是它获取数据之后要干什么?
消费者获取数据之后一定会做一些事情,比如把数据入库,把数据上传到网络等
特别是消费者拿到的是任务类的时候,消费者要执行拿到的任务!
即:一个消费者获取一个数据之后,需要时间"消化",在"消化"完成之前,这个消费者不会再去阻塞队列里拿数据!
!!
综上两点:
消费者和生产者不会不停地访问阻塞队列,因为它们更多的时间都在等数据(处理数据)
所以:
-
①在生产者阻塞等待新数据到来的时候,阻塞队列中很可能还有数据
只要阻塞队列中还有数据,任一个消费者都还可以从阻塞队列中获取数据,并处理数据
所以:
此时消费者线程和生产者进程不就是在并行执行吗? -
②消费者处理数据时,生产者也等待数据就绪
那么在所有消费者线程都忙着处理数据的时候,生产者们也可以获取数据并放进阻塞队列中!
所以:
此时消费者线程和生产者进程不就是在并行执行吗?
生产者线程和消费者线程,只有在阻塞队列里"交易"数据的时候是互斥的,串行的!
但是:
生产者线程和消费者线程,在阻塞队列里"交易"一次数据的花费的时间非常非常短,因为就是拷贝两次而已
而生产者从外部获取一个数据,消费者处理一个数据,要花费的时间远远比"交易"数据花费的时间长得多!
即整个生产者消费者模型真正串行执行的时间非常短,基本都是并行执行,生产者消费者模型的高效性是这样体现的