238_吸烟者问题
# 2.3_8_吸烟者问题
各位同学大家好。在这个小节中我们会学习一个进程同步互斥的问题叫做吸烟者问题。
程对缓冲区的访问一定这种关系。
一个系统当中有三个抽烟者进程和一个供应者进程,每个抽烟者进程都会不停的卷烟,并且把它抽掉,但是要卷起一支烟,抽烟者需要三种材材料,烟草纸和胶水。
这三个抽烟者分别拥有烟草、纸和胶水这样的材料,但每个抽烟者都缺少另外两种材料,而供应者他会无限的为这三个抽烟者提供他们所需要的材料,供应者会往这个桌子上放两种材料,比如说是纸和胶水,那么这两种材料是第一个抽烟者所需要的,所以第一个抽烟者会把这两种材料从桌子上取走,并且把它卷成香烟,然后抽掉。当他完成吸烟之后,吸烟者进程,他会向供应者进程发出一个信号,告诉他当前抽烟已经完成了。
那么供应者进程又会往桌子上放入下一组材料,比如说是烟草和胶水,那么这两个材料是二号抽烟者所需要的,所以他会从桌子上取走这两个材料,并且把它卷成香烟吸掉。和刚才一样,他也会通知供应者,现在吸烟已经完成
那么供应者又会接着往这个桌子上放下一种组合的材料烟草和纸,这个是三号抽烟者所需要的,所以他会把这个材料给取走,并且卷成香烟吸掉。
那么每一个抽烟者进程只会从桌子上拿走自己所需要的那两种材料,而这个供应者进程它需要让三个抽烟者轮流的吸烟,也就是说他需要轮流的为第一个进程进程提供他所需要的两种之后,又为进程提供他所需要的两种,再之后又为进程提供他所需要的两种,然后再来一个轮回,不断的无限的循环。
有没有发现这个问题其实也是一个生产者消费者的问题,供应者相当于是一个生产者,而这些吸烟者相当于是一个消费者,但是与之前咱们所介绍的生产者消费者模型不同的是,这个生产者他可能会生产三种不同的产品。
那么我们根据之前学过的那种思路来依次进行分析。首先题目当中所描述的进程总共有 4 个,很显然 1 个供应者进程和 3 个抽烟者进程,他们之间是否有同步互斥的关系,由于供应者进程每次只能往桌子上放一种材料的组合,所以我们可以把桌子看作是一种容量为 1,初始为空的缓冲区,而对缓冲区的访问需要互斥的进行,咱们在之前已经强调过这个问题。
为什么说它的容量唯一不是说桌子上每次会放两种材料吗?其实这个题目当中我们不能把两种材料看作是两种独立的物品,我们应该把它看作是一种组合。比如说纸和胶水的组合,我们就把它看作一个整体,称为组合一,组合一是抽烟者一需要使用的,而烟草和胶水的组合我们把它称为组合二,这是抽烟者二所需要的。组合三也一样,它是抽烟者三所需要的,所以其实我们不应该说这个桌子上可以同时放两种材料,而应该说这个桌子上同一时刻只能放其中某一种组合,我们把这些组合看作一个整体,这是这个题目当中隐含的一个互斥问题。
接下来我们再来看一下同步问题,只有桌子上有组合一,这个事件发生的时候,才可以发生第一个抽烟者取走东西这个事件。同样的,只有桌子上有组合二,只有这个事件发生的时候才可以接着发生,第二个抽烟者取走东西这个事件。那么对于这三对一前一后的这种同步关系来说,我们需要对这些关系各自设置一个同步变量。
另外题目当中还给了一个条件,只有抽烟者卷了一根烟,并且把它抽掉之后,他才会给供应者进程发送一个信号,告诉他现在抽烟已经完成了,之后供应者才会把接下来的两种材料放到桌上,所以发出完成信号这个事件发生之后,供应者才可以把下一个组合放到桌子上,所以这是第四个同步关系。
那么第二步我们再来看一下大体需要执行哪些 pv 操作。对于实现互斥的 pv 操作来说很简单,无非就是在访问临界资源的之前和访问临界资源之后,对互斥信号量分别执行 p 和 v 操作。
而对于同步问题来说,我们需要遵循前 v 后 p 这样的原则,必须发生在前面的事件发生了之后,我们需要执行一个 v 操作,而必须发生在后面的事件发生之前,我们需要执行一个 p 操作。
那么接下来我们就需要确定各个信号量的初始值到底是多少。对于互斥问题来说,由于缓冲区的大小唯一,所以我们即使不设置专门的互斥变量,其实也可以实现互斥访问临界区这件事情,咱们在之前小节已经介绍过类似的问题,所以我们只考虑设置这些同步信号量
桌上有组合一,这个事件必须发生在第一个抽烟者取走东西这个事件之前,所以对于这一对同步关系来说,我们需要为它设置一个同步信号量叫做 offer1,那么刚开始桌子上是没有组合一的,这个事件只能由供应者来触发,所以同步信号量的初始值我们应该把它设置为 0。而第二个和第三个关系也和刚才的类似,我们分别为这两对同步关系设置 offer2 和 offer3 这两个同步信号量,而初始值和刚才一样都是设为 0。
第四对同步关系发出完成信号这个事件要发生在供应者将下一个组合放到桌上这个事件之前。而发出完成信号,这个事件可以由这三个吸烟者当中的任何一个来触发。所以这三个吸烟者进程都需要对与这个事件相关的同步信号量执行一个 v 操作,而后面这个放入材料的事件只能由供应者来执行,所以供应者需要对这个事件对应的同步信号量执行一个 p 操作。那么由于刚开始并没有任何一个吸烟者进程发出了完成信号,所以我们为它设置的同步变量是 finish 的初始值也是 0
接下来我们再来看一下具体的代码实现。我们分别定义了 offer1、offer2 和 offer3,这三个同步信号量分别用来表示桌子上组合一、组合二、组合三的数量。另外还设置了一个叫做 finish 的同步信号量,用来表示此时这些抽烟者抽烟这个事件是否完成。
另外题目当中有个条件,说供应者需要轮流的给三个抽烟者供应材料,让三个抽烟者轮流的吸烟,所以我们设置一个 int 型的变量,用于实现三个抽烟者轮流抽烟这个事情。供应者每一次在桌上放了某一种组合之后,都需要对 i 这个变量执行加一,并且对三取余的这种操作,每一个循环就会使 I 的值变为 012,012 这样循环。那么如果这次循环中 i 的值为 0,那么供应者就就把组合一放在桌上,也就是让第一个吸烟者来吸烟。
如果 i 的值为一,那么它就让第二个吸烟者吸烟。i 的值为二,就让第三个吸烟者吸烟。而刚才我们也说过,i 的值会按照 012,012 这样的循环来往复的变化,所以这就可以实现供应者轮流的让三个吸烟者吸烟这件事情。
当供应者在桌上放入了某一种组合之后,它需要对这个组合对应的同步信号量执行一个 v 操作,用来通知等待这种组合的吸烟者。另外供应者把材料放到桌上之后,它需要等待吸烟者向他发出完成吸烟的信号。所以在最后这个地方我们又需要对 finish 信号量执行一个 p 操作。
而各个吸烟者在从桌子上拿走材料之前,需要检查此时桌子上放的是不是自己所需要的材料。当把这些材料拿走,并且抽掉之后,又需要向供应者发出一个完成信号来通知,供应者可以开始放下一个材料了。
所以由于刚开始 finish 这个信号量是 0,所以供应者它在放完第一组材料之后,它会在这个地方被阻塞,一直等到第一个吸烟者从桌上拿走组合一并且卷烟抽掉之后,他又会对 finish 执行 v 操作,从而又重新让供应者进程从阻塞态又回到就绪态,然后又可以进行下一轮的供给。而对于另外两个吸烟者来说,他们所做的事情和第一个吸烟者其实也类似,这就不再展开赘述。
这个地方大家再结合这些代码来分析一下,到底是不是需要专门的设置一个斥信号量,才可以保证各个进程他们互斥的访问桌子缓冲区。其实如果经过分析之后,大家会发现这些信号量同一时刻,只会至多只有一个的值可能是一,也就是说这 4 个进程在同一时刻最多只会有其中的 1 个不被 p 操作所阻塞,可以顺利的进入桌子临界区,这个地方感兴趣的大家可以自己再推一下。
# 小结
吸烟者问题为我们解决,可以生产多个产品的单生产者这种问题提供了一个可以参考的思路。那么我们比较值得借鉴的是这个题目当中我们是如何实现轮流让各个吸烟者吸烟的。我们设置了一个整形变量 i,并且我们让整型变量 i 按照 012,012 这样的顺序来循环的改变它的值,这样就实现了轮流让各个吸烟者吸烟这个需求。
如果说题目改为每次供应者会随机的让一个吸烟者吸烟的话,我们应该怎么写这个代码逻辑呢?其实在咱们的王道书上是写了一个 random 变量,这个 random 变量是一个随机数,但是题目中要求我们实现的是轮流让各个吸烟者吸烟,所以我认为王道书上给的代码逻辑应该是实现了每次随机的让一个吸烟者吸烟,而如果要轮流的让个吸烟者吸烟的话,我们需要按照刚才咱们讲的那种逻辑来实现。
另外如果一个生产者要生产多种产品或者说一个进程,它有可能会引发多种前驱事件。所谓的前驱事件就是说这个事件发生了之后,另一个事件才可以发生。那么在这种情况下,我们就要在各个前驱事件发生之后的位置,加上与它对应的 v 操作,就像这个题目当中供应者的那几个 v 操作一样,这是吸烟者问题值得大家借鉴的地方