CHS_08.2.3.6_1+生产者-消费者问题
- 问题描述
- 问题分析
- 思考:能否改变相邻P、V操作的顺序?
- 知识回顾
在这个小节中 我们会学习一个经典的进程同步互斥的问题
问题描述
并且尝试用上个小节学习的p v操作 也就是信号量机制来解决这个生产者消费者问题
问题的描述是这样的 在一个系统当中 有一组生产者进程和一组消费者进程
生产者进程每次生产一个产品并且放入缓冲区 那这缓冲区其实就是用来存放数据的
一片区域 我们可以把它理解为 一个一个小格子 每个缓冲区 也就是每一个小格子里面可以放一个产品
一个 也就是一坨数据 然后消费者呢 又会从这些缓冲区当中每次取出一个产品 也就是每次
取出一头数据 那生产者和消费者这两组进程他们会共享的使用
一个初始为空 大小为n的缓冲区 也就是有n个小格子可以放n个产品
那需要注意的是 这个缓冲区是有容量限制的 它只能放五个产品
那只有缓冲区没有满的时候 生产者才可以往这个缓冲区里放入产品
比如说像这个样子 因为缓冲区大小为五所以它最多只能放入五个产品 也就五个数据
那如果缓冲区此时已经满了 但是生产者进程还想尝试往里面写数据
那生产者进程必须阻塞等待 等这个缓冲区被取空的时候 他才可以往里边
写数据那有没有发现这个条件 就是我们上小节分析的同步问题
问题分析
也就是说 缓冲区没满的时候
生产者才能生产 这是一对一前一后的同步关系 另外呢
消费者进程 他会从这个缓冲区里取走这些产品 就是取走这些数据 就像这个样子
那当缓冲区没满的时候 也就是有空闲的缓冲区的时候 生产者就可以继续生产
也就是说 当一个消费者从缓冲区取走数据之后 如果此时有生产者是处于阻塞状态的 那么消费者进程应该把生产者进程给唤醒
让他重新回到就绪态 不过呢 生产者进程只是回到了就绪态 这并不意味着生产者进程会立即往里边写数据
所以接下来有可能是消费者进程继续往把这些缓冲区里的数据依次取走 那需要注意的是 只有缓冲区不空的时候 也就是说只有缓冲区里有数据 有产品的时候
消费者进程才可以从缓冲区里取走数据 不然的话消费者进程就必须阻塞等待
所以其实这个条件就是第二对的同步关系 也就是只有缓冲区没空的时候 或者说缓冲区里有产品的时候
消费者才可以取走数据 才可以消费 那最后一个条件是这个缓冲区 它是一种临界资源
各个进程必须互斥的访问缓冲区 那这我们来解释一下为什么必须互斥的访问缓冲区
那假设这个系统当中此时有两个生产者进程正在并发的运行 那么第一个生产者进程在检查缓冲区的时候发现 哎 这个缓冲区此时都是空的
所以我可以挑选其中的某一片往里面写入数据 与此同时 第二个生产者进程 它也并发的运行
他也发现此时这个缓冲区都是空的 因此他也会挑选其中的一块写入数据
那假设下面的这个生产者进程往这写了一篇数据 而上面这个生产者进程他自己挑选的也是这一片区域
那接下来这个生产者进程也往这片区域冲入自己的数据 那这样的话 他就会把前面那个进程的数据给覆盖掉
所以可以看到 如果各个进程同时访问缓冲区的话 那么有可能会出现一系列的问题 比如说刚才我们提到的数据覆盖
所以缓冲区这种资源 它是一种临界资源 各个进程必须互斥的访问
那么既然需要互斥的访问 这其实就是我们之前学过的互斥的问题 那刚才我们做的这一系列分析
找出了这个问题里边所隐含的一系列的同步和互斥的关系
那第二步我们需要做的是要根据各个进程操作的流程来确定pv操作的大致顺序
那其实很简单 就是我们上个小节中提到过的 前V后P 第一对关系是只有 当
缓冲区里有产品的时候 消费者进程 它才可以消费 另外呢 只有缓冲区没满的时候
生产者进程才可以生产 所以这样的两对一前一后的同步关系 我们就需要给他们分别设置一个同步信号量
并且在前面这个动作完成之后 需要对这个同步信号量执行一个V操作
在后面这个动作开始之前 需要对这个同步信号量执行一个p操作 那我们一会来解释一下这两个同步信号量 它在背后的含义
那这是这两对同步关系里边pv操作的一个大致顺序 那缓冲区里有产品
这个事件是生产者进程触发的 也就说 当一个生产者进程往缓冲区里放入一个产品 放入一个数据之后
他就需要对这个信号量执行一个V操作 而当消费者进程从缓冲区里取走数据之前
它需要对这个型号量执行一个p操作 那下面的这个同步关系也一样 那除了这两对同步关系之外 我们还需要实现对缓冲区这种临界资源的互斥访问
那互斥的实现其实是很简单的 我们只需要设置一个互斥信号量 mutex 并且让它的初始值为一
然后在临界区的前面和后面分别对互斥信号量执行p v操作就可以了 那接下来我们应该做的就是要确定这些信号量的初始值
对于互斥信号量来说 一般初始值就是一就像我们刚才所说的那样 而对于同步信号量来说 它的初始值 我们要看它所对应的那种资源初始值是多少
那我们来看一下这两对同步关系所对应的这种资源到底是什么呢 那先来看上面这对同步关系
消费者进程 他在消费之前需要消耗什么资源呢 需要消耗的是产品 对吧
所以他的这个p操作其实是在申请一个产品 申请一个数据
因此 full这个同步信号量 它所对应的资源应该是产品的数量
也就是非空缓冲区的数量 而从题目当中给的条件 我们知道缓冲区刚开始都是空的
也就是说 刚开始产品的数量 产品这种资源的数量应该是零因此
full这个同步信号量的初始值 我们就把它设置为零表示刚开始没有产品这种资源
而这种资源只能由生产者进程 生产了之后来释放 那再来看第二个同步信号量empty
那我们知道 生产者进程每生产一个产品 就需要消耗一个空闲的缓冲区
因此 empty这个同步信号量 它所对应的资源就应该是空闲缓冲区这种资源
它的数量就是空闲缓冲区的数量 当一个消费者进程取走了产品 也就是
做了消费的动作之后 他就可以释放一个empty 也就是释放一个空闲的缓冲区
那从题目给的条件 我们知道 刚开始缓冲区都是空的 有n个缓冲区 因此空闲缓冲区它的初始值应该是n
所以这样我们就确定了这两个同步信号量的一个初始值 那接下来就是代码的实现
生产者进程 他要做的事情就是不断的生产一个产品 并且把产品放到缓冲区
而消费者进程 他要做的事情就是不断的从缓冲区里取走一个产品 并且使用这个产品消费这个产品
那通过刚才的分析 我们知道 生产者进程再把产品放入缓冲区之前需要申请一个空闲的缓冲区
因此 当他放入产品之前 需要对empty这个同步信号量执行一个p操作
而当他把产品放入缓冲区之后 其实就相当于产品这一种
资源的数量加一了 或者说非空缓冲区的数量加一了 因此当它放入产品之后 需要对这个信号量执行一个V操作
表示 增加一个这种资源 而消费者进程的分析也类似当他从缓冲区取走一个产品之前
需要先对这个full变量进行一个p操作 也就是要申请消耗一个产品这种资源
而当他取走了一个产品之后 空闲缓冲区的数量就会加一因此在这个操作之后 需要对empty进行一个v操作 表示要增加一个空闲缓冲区
那这样我们就实现了两对同步关系 另外 题目中要求缓冲区必须互斥的访问 它是临界资源 所以在访问缓冲区
前后分别要对这个互斥信号量执行p和v操作 用于实现对缓冲区的互斥访问
那从这个例子当中 我们可以看到 实现互斥的p v操作 它是在同一个进程内进行的
这个进程对临界区上了锁 而当他访问完临界区之后 又需要的对这个临界区进行解锁
所以 这个pv操作是在同一个进程的代码当中实现的 而同步关系就不一样了 我们会发现
这个进程 生产者进程里边是对full变量执行了v操作 而消费者进程里边是对full变量执行了p操作
也就是对同一个变量的p v 这两个操作是分别需要在不同的两个进程之间执行的
执行v操作的这个进程会唤醒相对应的执行p操作的这个进程
那如果结合刚才我们给的这个同步关系的前驱图 就会发现
这个代码其实完全可以根据这个前驱图的关系来得出 那这大家可以自己暂停来
体会一下前V后P 那接下来我们要思考的一个问题是
能否改变相邻的pv操作的顺序呢 也就说我们刚才是先对同步信号量执行了p
思考:能否改变相邻P、V操作的顺序?
再对互斥信号量执行了p 而我们现在尝试把它颠倒一下 先对这个互斥信号量执行p操作 再对同步信号量执行p操作 会发生什么情况呢
我们来看一下 假设此时这个缓冲区里边已经放满了产品 也就是empty等于零
full等于n 那此时如果生产者进程执行了一使empty的值变为了零
接着 他在执行二这一句 而此时由于empty的值已经是零了 所以生产者进程会被阻塞在二这一句 他需要等待empty这种资源
那这个时候发生了调度消费者进程 上处理机运行 那消费者进程在执行三这一句的时候
他会发现此时empty的值已经是零了 所以消费者进程他会被阻塞在三这一句
他需要等待生产者进程释放mutex 所以这就导致消费者进程也被阻塞了
那我们在捋一捋这种情况下 生产者进程 他在等待消费者进程对empty执行v操作
而消费者进程 他又在等待生产者进程对mutex执行v操作
也就说 他们在相互等待对方唤醒自己 而两个人其实都被阻塞 都在睡眠
而那这种情况就是我们之后会学习到的死锁 进程和进程之间发生了循环等待
被对方唤醒这样的一个神奇的情况 那发生了死锁之后 这两个进程就都不能往下执行 都不能推进下去了
那同样的 大家可以分析一下 当full等于零当empty等于n的时候 如果按341这样的顺序执行 是不是也会发生死锁
所以在这个地方 我们要强调的是实现互斥的这个p操作
一定要在实现同步的p操作之后 他们的顺序是不能颠倒的 如果颠倒的话 那么就有可能出现我们这所提到的死锁的问题
所以这是大家需要特别特别注意的地方 那接下来我们再思考一下 如果V操作的这个顺序颠倒的话 会不会出现问题呢
其实是不会的 因为V操作并不会导致任何一个进程阻塞 所以他们俩的顺序颠倒并不会发生这种循环等待的问题 所以两个V操作的顺序是可以交换的
那最后我们再来考虑一个问题 生产者生产一个产品和消费者使用一个产品 是否可以放到pv操作之间呢
其实逻辑上看是可以放到p v操作这里边的 但是如果我们把这两部分的处理都放到p v操作里边的话
那么就会导致临界区代码变得更长 也就是说 一个进程对临界区上锁的时间会增长
那这样的话 肯定不利于各个进程交替的来使用这个临界区资源
所以我们要让临界区的代码尽可能的短 因此逻辑上来看 把这两个部分放进去是没有问题的
但是实际上会对系统的效能产生影响 因此并不建议把这两个部分的操作放到p为操作里面
好的 那么这个小节当中 我们介绍了一个很经典的进程的同步互斥问题 就是生产者消费者模型
那我们根据这个问题 给出了做pv操作相关题目的一个解题思路 这个解题思路我们会在之后的
那些问题讲解当中在继续应用那生产者 消费者问题 它是一个互斥和同步的综合问题 既包含了互斥又包含了同步
但是对于我们初学者来说 比较困难的是 要发现这个题目当中隐含的两对同步关系
有时候是生产者进程在等待消费者进程 有的时候又是消费者进程需要等待生产者进程
所以这是这个题目当中的难点 它是两对不同的 一前一后的这种关系
所以我们相应的也需要给他们设置两对不同的信号量 但是对这些信号量的操作肯定都是遵循一个前V后P的 这个
原则那根据上个小节的讲解 相信这个地方已经不难理解了 另外还需要再强调一遍的是
知识回顾
实现互斥和实现同步是两个不同的p操作而实现互斥的
那个p操作一定要在实现同步的p操作之后 否则会导致死锁的问题
这个大家在暂停回忆一下 那么在接下来的讲解当中 我们会适当的加快一点点速度 我们不会在像之前一样每个同步信号量都来分析他背后的含义 这个需要大家自己去练习和体会
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习