以下罗列一些在多道程序环境下,产生的一系列经典的进程同步问题。

生产者-消费者问题

问题描述

生产者-消费者(producer-consumer)问题是有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程将其所生产的产品放入一个缓冲区中,消费者进程可从一个缓冲区中取走产品去消费。只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。缓冲区是临界资源,各进程必须互斥地访问。
image.png
可以利用一个数组 buffer 来表示具有 n 个缓冲区的缓冲池,每投入或取出一个产品时,缓冲池 buffer 中暂存产品或或被取走产品的数组单元指针 in 或 out 需要移动,这些用代码描述如下。

  1. int in = 0; //输入指针
  2. int out = 0; //输出指针
  3. item buffer[n]; //缓冲区

由于 buffer 描述的缓冲池是循环队列结构,因此输入指针 in 或输出指针 out 表示成“in = (in + 1) % n” 和 “out = (out + 1) % n”,当 (in + 1) % n = out 时表示缓冲池满,in = out 则表示缓冲池空。

记录型信号量解法

可利用互斥信号 mutex 实现诸进程对缓冲池的互斥使用,利用信号量 empty 和 full 分别表示缓冲池中空缓区和满缓冲区的数量。

  1. semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
  2. semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
  3. semaphore full = 0; //同步信号量,表示非空缓冲区的数量

又假定这些生产者和消费者相互等效,只要缓冲池未满,生产者可将消息送入缓冲池。只要缓冲池未空,消费者便可从缓冲池中取走一个消息。应注意每个程序中用于实现互斥的 wait(mutex) 和 signal(mutex) 必须成对地出现,其次对资源信号量 empty 和 full 的 wait 和 signal 操作。
对生产者而言,可以用代码描述如下:

  1. void proceducer(){
  2. do{
  3. producer an item nextp; //生产一个产品
  4. wait(empty); //消耗一个空闲缓冲区
  5. /*实现互斥*/
  6. wait(mutex);
  7. buffer[in] = nextp; //产品放入缓冲区
  8. in = (in + 1) % n; //移动输入指针
  9. signal(mutex);
  10. /*实现互斥*/
  11. signal(full);
  12. }while(TRUE);
  13. }

对消费者而言,可以用代码描述如下:

  1. void consumer(){
  2. do{
  3. wait(full); //消耗一个产品
  4. /*实现互斥*/
  5. wait(mutex);
  6. nextc = buffer[out]; //产品拿出缓冲区
  7. out = (out + 1) % n; //移动输出指针
  8. signal(mutex);
  9. /*实现互斥*/
  10. signal(empty); //增加一个空闲缓冲区
  11. consumer the item in nextc; //使用产品
  12. }while(TRUE);
  13. }

整个生产消费者问题的流程,用代码描述如下:

  1. void main() {
  2. cobegin
  3. proceducer();
  4. consumer();
  5. coend
  6. }

AND 信号量解法

利用 AND 信号量来解决时,用 Swait(empty,mutex) 来代替 wait(empty) 和 wait(mutex),用 Ssignal(mutex, full) 来代替 signal(mutex) 和 signal(full)。用 Swait(full,mutex) 代替 wait(full) 和 wait(mutex),以及用 Ssignal(mutex,empty) 代替 Signal(mutex) 和 Signal(empty)。利用 AND 信号量来解决生产者-消费者问题的代码描述如下:

  1. int in = 0; //输入指针
  2. int out = 0; //输出指针
  3. item buffer[n]; //缓冲区
  4. semaphore mutex = 1; //互斥信号量,实现对缓冲区的互斥访问
  5. semaphore empty = n; //同步信号量,表示空闲缓冲区的数量
  6. semaphore full = 0; //同步信号量,表示非空缓冲区的数量
  7. void proceducer(){
  8. do{
  9. producer an item nextp;
  10. Swait(empty, mutex);
  11. buffer[in] = nextp;
  12. in = (in + 1) % n;
  13. Ssignal(mutex, full);
  14. }while(TRUE);
  15. }
  16. void consumer(){
  17. do{
  18. Swait(full, mutex);
  19. nextc= buffer[out];
  20. out = (out + 1) % n;
  21. Ssignal(mutex, empty);
  22. consumer the item in nextc;
  23. }while(TRUE);
  24. }
  25. void main() {
  26. cobegin
  27. proceducer();
  28. consumer();
  29. coend
  30. }

管程解法

利用管程来解决生产者-消费者问题时,首先便是为它们建立一个管程,并命名为 procducerconsumer(PC)。用整型变量 count 来表示在缓冲池中已有的产品数目,其中包括两个过程:

过程 说明
put(x) 生产者利用该过程将自己生产的产品投放到缓冲池中
get(x) 消费者利用该过程从缓冲池中取出一个产品

对于条件变量 notfull 和 notempty,分别有两个过程 cwait 和 csignal 对它们进行操作:

过程 说明
cwait(condition) 当管程被一个进程占用时,其他进程调用该过程时阻塞,并挂在条件 condition 的队列上
csignal(condition) 唤醒在 cwait 执行后阻塞在条件 condition 队列上的进程

PC 管程可描述如下:

  1. Monitor procducerconsumer {
  2. int in = 0; //输入指针
  3. int out = 0; //输出指针
  4. item buffer[n]; //缓冲区
  5. condition notfull, notempty; //条件变量
  6. int count = 0; //缓冲池中已有的产品数目
  7. public void put(item x){
  8. if(count >= N) //缓冲池已满
  9. {
  10. cwait(notfull); //生产者等待
  11. }
  12. buffer[in] = x;
  13. in = (in + 1) % N;
  14. count++;
  15. csignal(notempty);
  16. }
  17. public void get(item x){
  18. if (count<= 0) //缓冲池没有可用的产品
  19. {
  20. cwait(notempty); //消费者等待
  21. }
  22. x = buffer[out];
  23. out =(out+1) % N;
  24. count--;
  25. csignal(notfull);
  26. }
  27. }PC;

在利用管程解决生产者-消费者问题时,可用代码描述为:

  1. void producer(){
  2. item x;
  3. while(TRUE){
  4. produce an item in nextp;
  5. PC.put(x);
  6. }
  7. }
  8. void consumer(){
  9. item x;
  10. while(TRUE){
  11. PC.get(x);
  12. consume the item in nextc;
  13. }
  14. }
  15. void main() {
  16. cobegin
  17. proceducer();
  18. consumer();
  19. coend
  20. }

哲学家进餐问题

问题描述

一张圆桌上坐着 5 名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家只做思考和进餐两件事情,哲学家在思考时不影响他人,只有当哲学家饥饿时才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上则需等待,饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
image.png

解法

经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。

  1. semaphore chopstick[5] = {1,1,1,1,1};

所有信号量均被初始化为 1,当哲学家饥饿时总是先去拿他左边的筷子,成功后再去拿他右边的筷子便可进餐。进餐完毕时先放下他左边的筷子,然后再放他右边的筷子。

  1. do{
  2. wait(chopstick[i]); //拿起左边的筷子
  3. wait(chopstick[(i + 1) % 5]); //拿起右边的筷子
  4. eat
  5. signal(chopstick[i]); //放下左边的筷子
  6. signal(chopstick[(i + 1) % 5]); //放下右边的筷子
  7. think
  8. }while(TRUE);

除了利用记录型信号量,也可以使用 AND 型信号量来解决,这样的写法更为简洁。

  1. do{
  2. Sswait(chopstick[(i + 1) % 5], chopstick[i]); //拿起筷子
  3. eat
  4. Ssignal(chopstick[(i+1)%5],chopstick[i]); //放下筷子
  5. think
  6. }while(TRUE);

可能的死锁

假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量 chopstick 均为 0,当他们再试图去拿右边的筷子时,都将因无筷子可拿而无限期地等待。对于这样的死锁问题,可采取以下几种解决方法:

  1. 至多允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子;
  2. 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
  3. 奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定将是 1、2 号哲学家竞争 1 号筷子,3、4 号哲学家竞争 3 号筷子。即五位哲学家都先竞争奇数号筷子,获得后再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。

    读者-写者问题

    问题描述

    有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用。但若某个 Writer 进程和其他进程(Reader 进程或 Writer 进程)同时访问共享数据时,则可能导致数据不一致的错误。因此要求:

  4. 允许多个 Reader 可以同时对文件执行读操作;

  5. 只允许一个 Writer 往文件中写信息;
  6. 任一 Writer 在完成写操作之前不允许其他 Reader 或 Writer 工作;
  7. Writer 执行写操作前,应让已有的 Reader 者和 Writer 全部退出。

image.png

记录型信号量解法

为实现 Reader 与 Writer 进程间在读或写时的互斥,设置一个互斥信号量 Wmutex,再设置一个整型变量 Readcount 表示正在读的进程数目。又因为 Readcount 是一个可被多个 Reader 进程访问的临界资源,因此也应该为它设置一个互斥信号量 rmutex。

  1. semaphore rmutex = 1; //用于保证对 count 变量的互斥访问
  2. semaphore wmutex = 1; //用于实现对文件的互斥访问,表示当前是否有进程在访问共享文件
  3. int readcount = 0; //记录当前有几个读进程在访问文件

对 reader 而言,可以用代码描述如下:

  1. void reader(){
  2. do{
  3. wait(rmutex); //reader 进程互斥访问 readcount
  4. if(readcount == 0) //第一个 reader 进程开始读
  5. {
  6. wait(wmutex); //给共享文件“加锁”
  7. }
  8. readcount++; //访问文件的 reader 进程数加 1
  9. signal(rmutex);
  10. perform read operation; //读文件
  11. wait(rmutex); //各个 reader 进程互斥访问 readcount
  12. readcount--; //访问文件的 reader 进程数减 1
  13. if(readcount == 0)
  14. {
  15. signal(wmutex); //最后一个 reader 进程“解锁”
  16. }
  17. signal(rmutex);
  18. }while(TRUE);
  19. }

对 Writer 而言,可以用代码描述如下:

  1. void writer()
  2. {
  3. do{
  4. wait(wmutex); //写之前“加锁”
  5. perform write operation;
  6. signal(wmutex); //写之后“解锁”
  7. }while(TRUE);
  8. }

对于整个读者-写者问题过程,可以用代码描述如下:

  1. void main() {
  2. cobegin
  3. reader();
  4. writer();
  5. coend
  6. }

信号量集机制解法

此时读者一写者问题引入一个限制,最多只允许 RN 个读者同时读,为此又引入了一个信号量 L,并赋予其初值为 RN。通过执行 wait(L, 1, 1) 操作来控制读者的数目,每当有一个读者进入时,就要先执行 wait(L,1,1) 操作,使 L 的值减 1。当有 RN 个读者进入读后,L 便减为 0,第 RN + 1 个读者要进入读时,必然会因 wait(L,1,1) 操作失败而阻塞。

  1. int RN; //最多允许同时读取文件的 reader 进程数
  2. semaphore L = RN //保证最多只有 RN 个 reader 进程同时读
  3. semaphore mx = 1; //标志是否有 writer 进程在操作文件
  4. void reader(){
  5. do{
  6. Swait(L, 1, 1); //增加一个 reader 进程读文件
  7. Swait(mx, 1, 0); //无 writer 进程写文件
  8. perform read operation;
  9. Ssignal(L, 1); //减少一个正在读文件的 reader 进程
  10. }while(TRUE);
  11. }
  12. void writer(){
  13. do{
  14. Swait(mx, 1, 1; L, RN, 0) //无 reader 或 writer 进程在操作,“加锁”
  15. perform write operation;
  16. Ssignal(mx, 1); //writer 进程“解锁”
  17. }while(TRUE);
  18. }
  19. void main(){
  20. cobegin
  21. reader();
  22. writer();
  23. coend
  24. }

吸烟者问题

问题描述

假设一个系统有三个抽烟者进程和一个供应者进程,每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟需要有三种材料:烟草、纸和胶水。三个抽烟者中第一个拥有烟草,第二个拥有纸、第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料再桌上,这个过程一直重复。
image.png

解法

从事件的角度来分析,吸烟者问题有 4 个同步关系,分别是桌上有组合一时第一个抽烟者取走东西,桌上有组合二时第二个抽烟者取走东西,桌上有组合三时第三个抽烟者取走东西,最后是吸烟者发出完成信号,供应者将下一个组合放到桌上。因此需要设置 4 个信号量,来分别对应 4 个同步关系。

  1. semaphore offerl = 0; //桌上组合一的数量
  2. semaphore offer2 = 0; //桌上组合二的数量
  3. semaphore offer3 = 0; //桌上组合三的数量
  4. semaphore finish = 0; //抽烟是否完成
  5. int i = 0; //正在吸烟的吸烟者序号

对于材料提供者而言,可以用代码描述如下:

  1. void provider(){
  2. while(1){
  3. if(i == 0){
  4. 将组合一放桌上;
  5. wait(offer1);
  6. }
  7. else if(i == l){
  8. 将组合二放桌上;
  9. wait(offer2);
  10. }
  11. else if(i == 2){
  12. 将组合三放桌上;
  13. wait(offer3);
  14. }
  15. i = (i + 1) % 3;
  16. signal(finish);
  17. }
  18. }

对于 3 位吸烟者,可以用代码描述如下:

  1. void smoker1(){
  2. while(1){
  3. signal(offer1);
  4. 从桌上拿走组合一,卷烟抽;
  5. wait(finish);
  6. }
  7. }
  8. void smoker2(){
  9. while(1){
  10. signal(offer2);
  11. 从桌上拿走组合二,卷烟抽;
  12. wait(finish);
  13. }
  14. }
  15. void smoker3(){
  16. while(1){
  17. signal(offer3);
  18. 从桌上拿走组合三,卷烟抽;
  19. wait(finish);
  20. }
  21. }

参考资料

《计算机操作系统(第四版)》,汤小丹 梁红兵 哲凤屏 汤子瀛 编著,西安电子科技大学出版社