考虑生产者-消费者问题(也称作有界缓冲区问题)。两个进程共享一个公共的固定大小的缓冲区。
其中的一个,生产者,将信息放入缓冲区;另一个,消费者,从缓冲区中取出信息(该问题也可被推
广到m个生产者,n个消费者的情况,但出于简单起见,我们只考虑一个生产者,一个消费者的情况)。
一般考虑的调度算法:
但该算法会有出现错误的情况:
当count为0,消费者刚执行完(count == 0)的判断时,调度者启用了生产者的进程,生产者向缓冲区中加
入一条数据,并将count递增为1;
但由于生产者认为count值在被自己递增前是0,也就是说消费者此时在sleep(虽然消费者并没有sleep,
但生产者是这么认为的),因此生产者调用wakeup唤醒消费者;
收到wakeup信号的消费者会丢弃该信号(因为自己并没有在sleep),消费者继续从原处执行,即sleep()
被执行;
此时便出现了一个有趣的情况,调度者再一次启用生产者的进程后,由于count只增不减,因此永远也不会
再调用wakeup(consumer),消费者也将一直sleep下去,缓冲区会被填满。
这里问题的实质在于发给一个(尚)未睡眠进程的唤醒信号丢失了。如果它没有丢失,则一切都很正常。
一种快速的弥补方法是修改规则,加上一个唤醒等待位(wakeup waitingbit)。当向一个清醒的进程发送一
个唤醒信号时,将该位置位。随后,当进程要睡眠时,如果唤醒等待位为1,则将该位清除,而进程仍然
保持清醒。
尽管在本例中唤醒等待位解决了问题,但很容易就可以构造出一些例子,其中有两个或更多的进程,这时
一个唤醒等待位就不敷使用。我们可以再打一个补丁,加入第二个唤醒等待位,或者甚至是8个、32个,
但原则上讲这并未解决问题。
信号量的方法:
Dijkstra建议设两种操作:DOWN和UP(分别为推广后的SLEEP和WAKEUP)。对一信号量执行DOWN
操作是检查其值是否大于0。若是则将其值减1(即,用掉一个保存的唤醒信号)并继续。若值为0,则进程
将睡眠,而且此时DOWN操作并未结束。检查数值、改变数值、以及可能发生的睡眠操作均作为一个单一的
、不可分割的原子操作(atomic action)完成。即保证一旦一个信号量操作开始,则在操作完成或阻塞之前别的
进程均不允许访问该信号量。这种原子性对于解决同步问题和避免竞争条件是非常重要的。UP操作递增信号
量的值。如果一个或多个进程在该信号量上睡眠,无法完成一个先前的DOWN操作,则由系统选择其中的一
个(例如,随机挑选)并允许其完成它的DOWN操作。于是,对一个有进程在其上睡眠的信号量执行一次UP
操作之后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。递增信号量的值和唤醒一个进程同样也
是不可分割的。
}
我们看该方法,由于down和up都是原子操作,因此就没有了上一个算法出现的问题。另外,mutex提供了
互斥机制,down(&mutex);F1();up(&mutex); 由于mutex初值被置为1,因此保证了down和up两句之间的语句,
是不允许其它进程参与的。比如,F1执行过程中,调度者启用了另一个进程,其有down(&mutex);F2();
up(&mutex); 显然,down(&mutex) 将导致该进程被阻塞,因为mutex值为0。当F1执行完,执行up(&mutex)
后,由于mutex这个信号量上有进程阻塞在down(&mutex)上(将要执行F2()的进程),该被阻塞的进程将被
唤醒。mutex的值只在0和1中,扮演了锁的角色。
另外,需要注意,假设将生产者代码中的两个DOWN操作交换一下次序,将使得mutex的值在empty之前被
减1,而不是在其之后。如果缓冲区是满的,即empty为0,生产者将阻塞,mutex值为0。这样一来,当消费
者下次试图访问缓冲区时,它将对mutex的执行一个DOWN操作,由于mutex值为0,则它也将阻塞。这将造
成死锁。