天勤计算机考研笔记
PV操作中经典的生成者(producer)和消费者(consumer)问题:
问题分析
生产者—消费者问题,实际上就是二者共用了一块缓冲区(题中的仓库),所以生产者之间和消费者之间是互斥的关系,但同时生产者消费者之间又是协同的关系,所以将其归类为进程同步问题。
解决方案1
根据问题,我们很容易写出下面的代码,其中的item
即“产品”。
但执行之后可能会产生一个很严重的问题:因为程序是并发执行的,所以很可能导致producer()
进程执行到某条语句的时候,因为调度的原因,cpu被分配给了consumer()
进程。严重情况下,可能会导致二者都进入阻塞状态,如下图情况:
这种情况也称为死锁。
发生这种情况的根本原因,是因为对于producer
和consumer
这两个进程而言,本题中的“仓库”是临界资源,(访问仓库的代码putItemIntoBuffer()
称为临界区),多个进程同时访问临界资源,就可能导致问题,这个问题也叫竞争条件(Race condition)。
解决方案1.1—同步问题
为了解决竞争条件问题,就引入了信号量。
其实方案1中的itemCount
也是信号,但它仅仅解决了是否阻塞/唤醒合作进程,没能说明具体阻塞/唤醒几个进程,也就是说没能体现出量的概念。
所以我们换个方式,对于producer
,我们关注仓库中还有多少个空位置empty
,而对于consumer
则关注仓库中已放入多少个产品full
。这就达到不再使用临界资源的情况。如下图所示:
从图中可以看出,producer
实际上是在不断消耗空位置个数,如果cpu始终在执行producer
,那么empty
被消耗为空,producer
被阻塞,继续执行的话empty
小于0,其绝对值就等于被阻塞的进程数。
同理,full<0
的绝对值就是consumer
被阻塞的进程数。
注意:这里当empty和full都大于0时,表示的是空位数和产品数。但是当二者小于0时,代表的是阻塞的producer和consumer!!!
从上面可以看出,empty
和full
存在逻辑上相反的关系,即$empty=n-full$,但是在各自的进程中,其过程又具有相似性,因此我们可以抽象出一个值,记为value
,其结构体如下图所示:
图中的两个函数就是PV操作。
接下来就可以改写producer
和consumer
的代码:
先来看producer()
:
P操作:
1 | P(empty); |
传入参数empty
,如果empty
自减后小于0,则说明仓库中没有空位置,则把当前的producer
阻塞掉;如果empty
自减后大于0,则说明仓库中仍然有位置,则继续执行当前进程:
1 | putItemIntoBuffer();//往仓库中加入一个商品 |
再执行V操作:
1 | V(full) |
当前的进程使用完了临界资源,则需要将full
+1,即释放了一个资源,例子中也就是产品数+1。值得注意的是若full
+1<=0,那说明consumer
之前有阻塞。
S小于0应该是说没有临界资源可供使用,为什么还要唤醒进程?
V原语操作的本质在于:一个进程使用完临界资源后,释放临界资源,使S加1,以通知其它的进程,这个时候如果S<0,表明有进程阻塞在该类资源上,因此要从阻塞队列里唤醒一个进程来“转手”该类资源。比如,有两个某类资源,四个进程A、B、C、D要用该类资源,最开始S=2,当A进入,S=1,当B进入S=0,表明该类资源刚好用完, 当C进入时S=-1,表明有一个进程被阻塞了,D进入,S=-2。当A用完该类资源时,进行V操作,S=-1,释放该类资源,因为S<0,表明有进程阻塞在该类资源上,于是唤醒一个。
上面这段话,结合第一个引用中的内容更好理解。
这里之前理解上还有个小误区,唤醒了consumer
的进程是进入就绪态而不是执行态,因此当前的进程——这里是producer
,还是需要继续执行直到完成。
右边consumer
的操作和producer
类似。
解决方案1.2—互斥问题
方案1.1解决了同步问题,但仍然存在互斥问题。即我们假设putItemIntoBuffer()
函数中的代码如下:
1 | Buffer[i]=item; |
这样如果当producer-1
执行到第一行语句时,cpu发生了调度,此时又有个进程producer-2
执行了第一行语句,那么显然Buffer[i]
中的元素就被覆盖掉了。
为了处理这个问题,我们只需要在一个进程执行上面两行代码时,不允许任何其他进程介入即可,如下图:
注意一下empty
、full
和mutex
的初值。
最后得出一些小结论: