236_生产者-消费者问题
# 2.3_6_生产者-消费者问题
各位同学大家好,在这个小节中我们会学习一个经典的进程互斥同步问题叫做生产者消费者问题。 在一个计算机系统当中会有一组生产者进程和一组消费者进程,生产者进程每一次会生产一个产品,并且把它放入缓冲区,而消费者进程每次会从缓冲区当中取出一个产品,并且把它使用。这个地方的产品,我们可以把它理解为是某种数据,生产者每生产者进程,每次生产一个数据把它放到缓冲区,而消费者进程每一次会从缓冲区当中取出一个数据,并且对这个数据进行处理,也就是所谓的使用。
那么生产者和消费者显然他们需要共享使用一个缓冲区,比如说像这个样子
刚开始缓冲区的是空的,并且大小是n。一般来说题目当中会给出具体的 n的数值,比如说大小为5的缓冲区,在刚开始由于缓冲区全部是空的,所以生产者进程可以生产一些产品把它放入到缓冲区当中,就像这个样子,一直到缓冲区被充满了之后,如果此时生产者进程他还想继续生产产品,并且把它冲入缓冲区的话,这个行为很显然应该是被阻止的,因为此时缓冲区的这些数据这已经被装满了,只有缓冲区腾出别的空闲的空间之后,生产者进程才可以继续往里边放数据
所以在这个时候只能切换为消费者进程,来消费这些数据,也就是从缓冲区当中取走其中的一些产品或者说数据,比如说像这个样子,只要缓冲区当中的只要缓冲区中有一个,或者大于一个的空闲的空间,那么此时就可以唤醒生产者进程,让他从阻塞又重新回到就绪队列。当然这个唤醒并不意味着生产者进程就立即上处理机运行,它只是回到了就绪队列而已。
所以接下来有可能是消费者进程继续执行。那么每一次在每一轮执行,他都会从缓冲句子当中取走一个产品,并且使用一直到缓冲区被取空了之后,如果此时消费者进程还继续尝试从缓冲区当中取走产品的话,由于此时已经为空了,那么这个时候这种取产品的行为应该是被阻止的,所以消费者进程应该被阻塞。而只有生产者进程在往里边放数据的时候,消费者进程才可以再重新被唤醒,又重新回到就绪队列。
这个地方我们还需要注意,缓冲区它是属于一种临界资源,各个进程是必须互斥的访问的,为什么呢?假如说我们的系统当中有两个生产者进程,此时这两个生产者进程在检查了之后,发现缓冲区的这些位置每个地方都是空的,生产者进程它可能就往这个位置冲入了一个它自己的产品,也就是数据
而另一个生产者进程在之前的检查当中也发现缓冲区所有的地方都是空的,那么在并发的环境下就有可能导致生产者进程,它同时在检查了之后也往这个地方冲入了一个数据,所以这就导致了前者的数据被后者的数据给覆盖的情况,因此我们是必须保证缓冲区是被互斥的访问的。
那么对于这个问题来说,我们应该怎么实现它题目当中所给出的这些逻辑这些功能?一般来说考试中考察的是使用pv操作,也就是咱们在之前的小节当中学过的信号量机制,来实现这些进程的同步和互斥的问题。
在上一个小节的学习当中,我们知道信号量机制可以用于实现进程的互斥,在访问临界区之前,对这个信号量执行p操作。访问临界区之后,对信号量执行v操作
另外我们在上个小节的末尾还强调过,除了互斥和同步问题,还可以用于实现对于一类系统资源的申请和释放这样的一个事情。这种情况其实就类似于上个小节当中咱们举到的打印机的例子,我们可以设置一个信号量,这个信号量的初值就是那种资源所对应的初始的数量,其实这个对于系统资源的申请和释放这种问题,本质上其实也属于同步问题。
因为如果说此时系统当中没有空闲的资源,可以分配给进程,那么如果此时有一个进程申请使用这种资源的话,进程就会被阻塞,而一直要等待其他的拥有资源的进程,释放了这种资源之后,被阻塞的进程才可以又再次被唤醒。
所以如果说系统当中的可分配资源为0,那么这种情况下,申请资源的进程需要在释放资源的进程之后才可以执行,所以这也是一前一后的这种同步问题。因此上个小节当中的那些问题,我们本质上都可以归为互斥和同步两个大类。
对于生产者消费者问题这样的pv操作的题目来说,我们可以用这样的思路来分析。
首先我们要根据题目的描述来分析,题目当中给出的场景当中到底只描述了几种或者说几个进程。这个题目很显然它描述了两类进程,第一是生产者进程,第二是消费者进程。在确定了有几种进程之后,我们还需要分析这些进程之间有没有存在同步关系或者互斥关系。这个题目当中比较容易发现的是缓冲区这种临界资源各个进程需要互斥的访问,所以各个进程对缓冲区的访问一定这种关系。
另外我们来看这句话,只有缓冲区没满时,生产者才可以从把产品放入缓冲区,否则必须等待,这显然是一种。因为如果缓冲区满了,那么生产者他需要等待消费者取走产品,也就是出现了我们所说的一前以后的问题。一在缓冲区满的时候,一定要等消费者先取走产品之后,才能由生产者继续往缓冲区里面放产品。
再看第三句话,只有缓冲区不空时,消费者才能从中取走产品,否则必须等待,这又是另外一个同步关系。在缓冲区为空的时候,也就是没有产品的时候,消费者需要等待生产者放入产品,这也出现了我们刚才所说的一前一后这样的关系,所以这也是同步关系。
在确定了这些同步互斥关系之后,我们再来看一下我们应该大致用什么样的pv操作来实现这些同步和互斥的关系。首先生产者每一次它需要消耗一个空闲的缓冲区而消耗一个资源,其实就是对这个资源对应的信号量执行p操作。如果说此时缓冲空闲缓冲区的数量为0,那么生产者是会被阻塞的。一直要等到消费者释放一个空闲缓冲区,也就是对空闲缓冲区对应的同步信号量执行v操作之后,生产者进程才可以再次被唤醒,然后重新把重新生产产品。
另外生产者进程他在确认了空闲缓冲区的数量是足够的情况下,它会生产一个产品,也就是对产品对应的同步信号量需要执行一个v操作。
而消费者在消耗一个产品之前,需要对这个产品对应的同步信号量执行一个p操作,所以这个v和这个p是一对,而这个p和这个v又是一对。
另外我们还需要实现互斥的访问缓冲区,刚才咱们已经说过我们需要设置一个互斥信号量,在临界区之前和临界区之后分别执行p操作和v操作。因此在这个题目当中,我们需要实现三对pv操作,分别对应三个不同的信号量。
那么接下来我们就需要确定我们需要设置的信号量具体是哪些,我们根据刚才分析出的这些互斥和同步关系,我们为每一对关系设置一个信号量,那么互斥信号量的初始值一般来说是一,在之前的小节当中咱们已经分析过,而对于同步信号量来说,它的初始值我们需要看与这个信号量对应的那种系统资源的初始值到底是多少。
因此我们根据题目给出的条件得到了这样的结论。首先互斥信号量初始值是一,这个不用解释。另外生产者进程在每一次往缓冲区里放入一个产品之前,一定需要检查此时空闲缓冲区的数量到底是多少,而由于我们的缓冲区大小为n,刚开始为空,所以空闲缓冲区的数量初始值,我们应该把它设置为n。
另外消费者在消费一个产品之前,他需要先检查此时缓冲区当中到底有多少个产品,因此我们还需要设置一个同步信号量,用来表示此时缓冲区当中的产品数量,也就是非空缓冲区的数量。那么由于刚开始缓冲区是空的,所以同步信号量的初始值应该是0。
semaphore mutex = 1 //互斥信号量,实现对缓冲区的互斥访问
semaphore full = n //同步信号量 表示空闲缓冲区的数量
semaphore empty = n //同步信号量,用于表示产品的数量,也即空闲缓冲区的数量
2
3
那么在确定了这些信号量的意义和它的初始值之后,我们再来看一下具体的代码。生产者进程他所需要做的其实就是两件事,不断的生产一个产品,然后把那个产品放入缓冲区。
而消费者进程他所需要做的事情,从缓冲区取出一个产品,然后再把那个产品给使用掉。通过刚才的分析,我们知道生产者进程在把产品放入缓冲区之前,需要检查此时是否有空闲的缓冲区,也就是对empty同步信号量要执行一个p操作,消耗一个空闲缓冲区。
当他把产品放入到缓冲区之后,又需要对full,也就是对产品数量同步信号量执行一个v操作,表示此时要增加一个产品,而消费者进程也类似,在刚开始他需要对此时的产品数量进行检查,对它进行一个p操作,表示要消耗一个产品,或者说要消耗一个非空缓冲区。当他从缓冲区当中取走一个产品之后,那么缓冲区当中又会出现一个新的空闲,空闲的那种区间,所以又需要再执行一个对empty变量的v操作表示,此时要增加也增加了一个空闲缓冲区。
那么除此之外,我们还需要实现对缓冲区的互斥访问,而互斥访问其实就是在临界区之前和临界区之后,分别对互斥信号量执行p操作和v操作,对于消费者经常来说也一样。这个地方大家有没有发现,
。那么在生产者消费者问题当中,我们会发现在这两个进程当中,分别连续的使用了两个p操作,第一个p操作适用于实现进程同步的,而第二个p操作是用于实现进程互斥的
那么我们能不能把这两个p操作的顺序给颠倒?就像这个样子
如果说我们是先执行互斥的p操作,再执行同步的p操作的话,会发生什么情况?假设此时缓冲区当中已经放满了产品,那么也就意味着此时空闲缓冲区的数量为0,而非空缓冲区,或者说产品的数量应该是n。
此时如果生产者进程先执行了一这个语句,那么互斥信号量就会从1变为0。之后他在执行二这个语句的时候,由于此时已经没有可以消耗的空闲缓冲区了,所以他在执行 p操作之后会被阻塞。
由于此时生产者进程被阻塞,因此只能切换回消费者进程执行。他先执行了三这个语句,发现此时mutex的值已经是0,也就是说它不允许被进入临界区,因此消费者进程也会被阻塞,这样的话这两种进程都被阻塞了,并且生产者进程在等待消费者进程释放一个空闲缓冲区,而消费者进程又在等待生产者进程,释放这个临界区的访问权限,也就是说他们俩发生了循环等待的现象,双方都在等待着被对方给唤醒,死锁是之后的小节当中会学习的,这个地方大家只需要知道发生了死锁的话,两这两类进程就都没办法往下推进了,所以我们得出结论,在。
另外如果说缓冲区当中没有产品,也就是full为0,empty为n这种情况下,如果按341这样的顺序执行的话,也会发生死锁。大家根据刚才的讲解过程再自己来进行分析一下。
除了p操作之外,对于v操作来说,由于v操作不会导致进程进入阻塞的状态,所以v操作的顺序交换是不会出现问题的,他们是可以交换的。
接下来我们再来考虑第三个问题,这地方大家会发现生产者生产产品和消费者使用产品这两个操作,他们都是放在各自的这些pv操作之外的,这些操作能不能放到pv之操作之内,其实逻辑上来看是没有问题的。我们可以让消费者从缓冲区取出一个数产品之后,就立即紧接着使用产品,但这会导致临界区的代码量变大。那么消费者进程在访问临界区的过程当中就需要耗费更长的时间,如果此时有别的进程也想访问临界区的话,它是会被阻塞的。所以对于这两部分的代码,我们最好不要放到pv操作之间。
# 小结
那么在这个小节中,我们学习了生产者消费者问题,大家一定要注意体会这个问题的分析思路,可以根据提纲自己再来回忆一下,能不能自己分析出来。其实生产者消费者问题,它无非就是互斥和同步相组合的一个综合问题,对于初学者来说,互斥关系其实是很好发现的,并且互斥的实现一般来说都不难,但是这个题目的难点在于要发现两对同步关系。
从题目给出的条件中,我们知道当缓冲区为空的时候,消费者需要等待生产者生产,而当缓冲区为满的时候,生产者又需要等待消费者消费,就像这个样子,所以这是两个不同的一前一后的这种同步问题,所以它也需要设置两个不同的同步信号量来分别实现这两个同步关系。
这个地方有没有发现,这其实就是我们上个小节提到的所谓的前驱图。那么如果我们需要实现像这种一前一后的同步关系的话,我们需要对应的信号量实现实行前v后p的操作,也就是说如果说缓冲区为空,那么生产者生产需要在消费者消费之前,因此在生产者生产之后,我们需要执行v操作,这就是前v的意思,在前面操作之后执行v操作,而在后面操作之前执行p操作。
对于另外同步关系也一样,也是前v后p。最后我们需要强调的是这个问题比较容易错的地方,用于实现互斥和实现同步的两个p操作的先后顺序问题,咱们之前具体讲过,实现互斥的操作一定要在实现同步的操作之后,否则会发生死锁现象。好的,那么以上就是小节的全部内容。