CHS_09.2.3.6_2+多生产者-多消费者
- 问题描述
- 问题分析
- 如何实现
- 如何实现
- 假如我们把盘子的容量设为二
- 知识回顾
在这个小节中 我们会学习一个多生产者 多消费者的这样一个问题模型
问题描述
先来看一下问题的描述 假设桌子上面有一个盘子 每次只能向这个盘子里放一个水果
有四个人 父亲 母亲 女儿和儿子 父亲每一次会向盘子里放一个苹果
而女儿专门等着吃盘子里面的苹果 所以如果盘子里有苹果的话 女儿会把
这个苹果给取出并且把它吃掉 另外母亲会专门往盘子里面放橘子
儿子又专门等着母亲把橘子放到盘子里 之后他会把盘子里的橘子给取出并且吃掉
如果说由于这个盘子的容量是只能装一个水果 所以如果父亲把苹果装到了盘子里的话
那么 如果这个苹果还暂时没有被女儿取出 这个时候母亲又包了一个
橘子 并且他尝试把橘子放到这个盘子里 但由于这个盘子里只能装一个水果 所以 这种
这个时候 母亲把橘子放入盘子 这个行为应该被阻止 另外呢 由于儿子只吃橘子 所以此时如果盘子里装的是一个苹果的话 那么如果这个儿子
正在尝试从盘子里取出水果 那这个水果由于不是他想吃的 所以它的这种行为也应该被阻止或者说阻塞
那这个问题 其实我们可以把它抽象为咱们上一个小节学过的生产者 消费者模型 我们可以把
问题分析
这个盘子看作是一个大小为一初始为空的缓冲区 然后把父亲和母亲看作是两个生产者进程
把女儿和儿子分别看作是两个消费者进程 当然 与上一节所讲的生产者消费者模型不同的是
这个这个小节中的这个例子 这些生产者和这些消费者 他们所生产的东西和他们所消费的东西类型是不一样的
而上个小节我们介绍的例子当中 所有的生产者生产的都是一种东西 而所有的消费者也都是消费同一种东西 所以这就是为什么把这个小节的这个问题称作多生产者多消费者的
原因 所谓的多 不是指多个 应该把它理解为是多类 不同类别的生产者和不同类别的消费者 他们所需要生产的和所需要消费的
这些产品是不一样的 那么这个问题我们怎么用pv操作来解决呢
我们用上个小节学习过的方法来一步一步一次分析 首先我们来看一下这个题目当中所描述的场景
总共有几类进程 很显然 父亲母亲 女儿儿子 他们各属于一类进程
另外呢 他们之间是否存在同步和互斥的关系 之前我们说过
这个题目当中这个盘子 我们可以把它看作是一个缓冲区 而上一节我们聊过
对于缓冲区的访问 一般来说都需要互斥的进行 所以我们需要实现对盘子这种缓冲区的互斥访问
另外 是否存在这种一前一后的同步关系呢 首先
父亲要将苹果放入盘子之后 女儿才能取到苹果 所以父亲进程和女儿进程他们之间有一对同步关系
另外 母亲要把橘子放入盘子之后 儿子才可以取到橘子 所以他们俩之间也存在一对同步关系
第三只有盘子为空的时候 父亲或母亲才可以从 才可以把水果放到盘子当中
而这个地方的盘子为空 这个事件其实既可以由儿子触发 也可以由女儿触发 假如说盘子里放的是橘子 那么盘子为空 这个事件就应该由
儿子来触发 由儿子取走橘子 而如果说盘子里放的是苹果的话 那就应该由女儿取走苹果 然后由女儿来触发 盘子为空这个事件
而只有盘子为空这个事件发生之后 才能允许父亲进程或母亲进程往盘子里放入水果
所以女儿和儿子 他们可能会触发一个事件来引发父亲或者母亲
往盘子里 放入水果 那这就是这个题目当中所包含的互斥关系和同步关系
第二步 我们来看一下各个进程之间的pv操作流程大概是什么样的
我们知道实现互斥其实很简单 无非就是在访问这个互斥临界资源的
之前和访问临界资源之后 分别对互斥变量实行一个p操作和一个v操作
而实现同步关系 其实我们之前也说过 我们只需要遵循一个原则 就是所谓的前V后P
也就是前面的这个事件发生了之后 我们需要执行一个V操作
而后面的这个事件发生之前 我们需要执行一个p操作 那就是这个样子
那么 对于实现互斥关系来说 我们当然是需要设置一个
初值为一的这种互斥信号量 而对于这些同步关系来说 我们需要根据具体的情况来判断每一个同步变量应该设为多少
父亲进程需要把苹果放入盘子 女儿才能取到苹果 所以我们需要设置一个同步信号量叫做apple用来实现这个同步关系
而由于刚开始盘子里面是没有苹果的 所以我们把这个值设为初始值 设为零
只有父亲对这个同步信号量执行v操作 之后 女儿对这个信同步信号量执行的p操作才不会被阻塞 那同样的 对母亲进程来说 我们也设置一个叫做orange 也就是橘子的一个同步信号量 他的初始值也设为一因为刚开始盘子里面是没有橘
另外呢 只有盘子为空的时候 父亲和母亲才可以放入水果 而刚开始
盘子本来就是空的 所以父亲和母亲在刚开始其实就可以向盘子里放入一个
水果 所以这个地方像这一对同步关系 我们就需要为它设置一个初值为一的同步信号量 用来表示此时盘子是否为空 那么来看一下具体的代码实现 我们总共申明了四个
如何实现
信号量一个 其中一个是互斥信号量 另外三个是同步信号量 父亲进程和母亲进程做的事情就是不断的准备一个自己的水果
然后再把自己的水果放到盘子里 而女儿进程和儿子进程做的事情就是不断的从盘子中取出自己喜欢的水果 并且把这个水果给吃掉
那父亲进程在放入水果之前需要先检查这个盘子是否为空
所以 在苹果放入盘子之前 父亲进程需要对这个信号量进行一个p操作来检查此时盘子中到底还可以放多少个水果
那如果这个苹果已经放入盘子之后父亲进程又需要对
apple这个同步信号量执行一个v操作 用来让apple的值加一
来告诉女儿进程 此时苹果盘子中的苹果数量已经加一了
而母亲进程也一样 他在把橘子放入盘子之前需要同样需要对盘子中还可以放多少个水果进行检查
如果说此时这个盘子中已经有别的水果 那么母亲进程会被阻塞 在这个地方会被阻塞
而当母亲进程把橘子放入盘子之后 他同样也需要对orange这个同步信号量执行一个v操作来通知儿子进程 此时盘子当中的橘子数已经加一了
那么女儿进程和儿子进程在取出自己喜欢的水果之前 分别需要检查此时这个盘子当中是否已经有自己喜欢的水果 所以女儿进程是对apple这个信号量执行p操作 而
儿子进程是对orange这个信号量执行p操作 而女儿进程在从盘子中取出苹果之前需要先检查此时苹果的是盘子中苹果的数量是否足够 如果没有苹果的话 它将被阻塞
而当他把苹果取出之后 又需要对plate这个信号量执行v操作 用来告诉父亲进程和母亲进程 此时盘子已经变空了
那么儿子进程也和女儿进程也类似 只不过是他在检查的时候 是检查盘子当中是否有橘子
那这样的话 我们就实现了咱们在这个图中表示的这些同步关系 另外 我们还需要实现各个进程对盘子这种缓冲区的互斥访问
所以我们在这些进程访问盘子之前 对这个互斥信号量执行一个p操作 访问之后又执行一个V操作 分别是对这个临界区进行加锁和解锁
其他的这些进程也一样 那接下来我们再来考虑一个问题 可不可以不要这个互斥信号量呢
如何实现
就是这样子 我们把互斥信号量去掉 并且把对互斥信号量的pv操作也都去掉
那我们来分析一下 如果是这样的话 这些进程会怎么并发执行 刚开始
由于apple和orange这两个信号量的数量都为零所以女儿进程和儿子进程无论谁上处理机运行 肯定在执行到这个p操作的时候都会被阻塞
那么 我们假设刚开始是由父亲进程上处理机运行 那么他首先会对盘子这个同步信号量执行一个p操作 由于刚开始这个信号量的值位一也就是说盘子这种资源还足够 所以父亲进程 他可以顺利的跳过这个p操作 然后开始
把苹果放入盘子当中 而这个时候 如果说切换回母亲进程 那么当母亲进程对盘子的信号量执行p操作的时候 由于这个值已经变为了零
盘子这种资源已经不够了 所以母亲进程 在这个时候会被阻塞等待盘子
而当父亲进程把苹果放入盘子之后 他又会对apple这个同步信号量执行一个V操作
这个时候 女儿进程她又会被唤醒 然后从盘子当中取出苹果 之后 女儿进程又会对plate这个同步信号量执行一个V操作
由于之前母亲进程是因为这个信号量而被阻塞的 所以当他执行v操作之后 母亲进程就会被唤醒
之后母亲进程就可以开始顺利的访问这个临界区资源 而当母亲进程在访问盘子这种临界资源的时候
由于plate的值为零然后orange和apple的值也都为零所以除了母亲进程以外 别的这些进程即使上处理机运行 也肯定会被卡在这个p操作 这也就会被阻塞
所以通过刚才的分析 我们会发现 在这个题目当中 我们即使不设置专门的互斥信号量没有mutex
我们依然可以实现这些进程对盘子这种临界区的互斥访问
为什么呢 其实原因在于这个题目当中的这个缓冲区的大小只为一
大家可以自己再尝试分析一下更多的情况 apple orange和plate
这三个同步信号量同一时刻最多只有一个 会是一而这几个进程刚开始都需要对其中的某一个信号量执行p操作 由于这三个同步信号量当中同一时刻最多只有一个的值会是一所以这些进程执行各自
的p操作的时候 最多只有一个进程不会被阻塞 可以顺利的进入这个临界区进行访问
那假如我们把盘子的容量设为二也就是这个缓冲区的容量把它设为二的话 会发生什么情况呢
假如我们把盘子的容量设为二
假设刚开始盘子就可以放两个水果 那么刚开始父亲进程执行p操作 发现盘子资源足够 所以他可以进入临界区开始访问盘子
而母亲进程在执行p操作之后也发现盘子这种资源依然是足够的 所以他同样也会进入这个
临界区对盘子这种临界资源进行访问 所以这就发生了父亲进程和母亲进程两个进程同时访问盘子这种临界资源的情况
那通过上个小节的讲解 我们知道 如果两个生产者进程 他们同时对一个缓冲区进行访问的话 那么有可能会导致数据覆盖的问题
这个地方也一样 因此如果我们在生产者 消费者问题当中遇到了缓冲区大于一的情况 那么我们就必须设置一个互斥信号量
mutex来保证各个进程是可以互斥的访问缓冲区的 而如果缓冲区大小等于一的话 那么我们即使不设置这个互斥信号量 有可能也可以实现互斥访问临界区这个事情 当然这不是绝对的 只是有可能 不
需要设置互斥信号量要具体问题具体分析 如果大家在考试的时候遇到缓冲区大小为一的情况的时候 那么
可以自己分析一下 如果能确定不需要使用互斥信号量的话 那么不设置也可以 但如果来不及仔细分析的话 大家最好是加上这个互斥信号量 因为加上了肯定也没错 不过我们需要注意的是上个小节强调过的那个问题实现互斥的
对于mutex 那个信号量的p操作一定要在实现同步的p操作之后 否则是有可能会引起死锁的
知识回顾
这个点大家还能不能回忆起来呢 那么和之前的小节一样 大家也需要体会p v操作这种相关的题目的解题思路
和上个小节介绍的经典的生产者消费者问题不太一样的是 这个小节这个模型是多生产者多消费者
他的同步关系要比之前那个小节所介绍的同步关系要复杂的多 大家需要注意一个事情
在分析这种同步问题的时候 我们不能从单个进程的行为的角度来触发 而需要把这种一前一后的事情
把它看作是两个事件的前后关系 这句话我们不太容易理解 我们直接来看例子
如果说在这个题目当中 我们是从单个进程行为的角度来考虑的话
那么 我们会有这样的结论 首先 从题目当中我们可以知道 如果盘子里边装的是苹果
那么一定要女儿取走苹果之后 母父亲和母亲才可以放入水果
所以 当女儿进程取走苹果之后 可能会导致父亲进程
可以放入水果 同样也可能导致母亲进程 可以放入水果 另外
如果盘子里装的是橘子话 那么儿子取走橘子之后 可能会导致父亲进程可以放入水果 也可能会导致母亲进程可以放入水果
那么如果我们从这个 这种单个进程的行为来分析的话 那仅仅这两句话 我们就可以分析出这样的四个同步关系
那么有四个同步关系是不是就意味着我们要设置四个同步信号量来分别实现这四个一前一后的这种关系了呢
当然不是其实正确的分析方法 我们应该从事件的角度来考虑
我们应该把刚才所描述的这种进程行为的前后关系 也就是 女儿取走
取走水果这个行为导致父亲可以放入水果 同样也导致母亲可以放入水果 从这种行进程行为的前后关系 我们可以把它抽象为一对事件的
前后关系 我们应该把进程同步问题 把它理解为 某一个事件 一定要求发生在另一个事件之前
而不是某一个进程的行为要发生在另一个进程的行为之前 那么我们如果从事件的角度来考虑的话
刚才所描述的这两句话 其实我们可以抽象为两个事件 盘子变空的事件可一定要发生在放入水果这个事件之前
而盘子变空这个事件 既可以由儿子进程来引发 也可以由
女儿进程来引发放水果的这个事件 既可以是父亲进程来执行也可以是母亲进程来执行
所以刚才我们看起来有四对同步关系 其实我们如果把从事件的角度来考虑的话 我们可以把它抽象为一对事件的前后关系
而这些事件可以由哪个进程来引发 那么这个进程就需要对这个事件对应的
同步信号量 执行一个v操作 同样的父亲进程和母亲进程都有可能会执行放入水果这个事件 所以在放他们执行各自的放入水果事件之前
我们需要对这个事件对应的同步信号量执行 分别执行一个p操作
那这个地方大家还需要自己认真的体会 这对于初学者来说确实比较容易犯这样的问题
那如果能把这个部分的内容理解了 以后再做同步相关的这些大题的时候 应该问题就不大了
好的 那么这就是这个小节的全部内容
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习