当前位置: 首页 > news >正文

操作系统---经典同步问题

文章目录

  • 1. 生产者-消费者问题
    • 1.1 问题描述
    • 1.2 问题分析
    • 1.3 解决问题
    • 1.4 问题扩展思考
    • 1.5 总结
  • 2. 多生产者-多消费者问题
    • 2.1 问题描述
    • 2.2 问题分析
    • 2.3 解决问题
    • 2.4 问题扩展思考
    • 2.5 总结
  • 3. 吸烟者问题(生产者-消费者问题的推广)
    • 3.1 问题描述
    • 3.2 问题分析
    • 3.3 解决问题
    • 3.4 总结
  • 4. 读者写者问题
    • 4.1 问题描述
    • 4.2 问题分析
    • 4.3 解决问题
    • 4.4 问题扩展思考
    • 4.5 总结
  • 5. 哲学家进餐问题
    • 5.1 问题描述
    • 5.2 问题分析
    • 5.3 解决问题
    • 5.4 总结
  • 6. 管程
    • 6.1 为什么要引入管程
    • 6.2 管程的定义和基本特征
    • 6.3 扩展1
    • 6.4 扩展2
    • 6.5 总结

1. 生产者-消费者问题

1.1 问题描述

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)。生产者、消费者共享一个初始为空、大小为n的缓冲区。只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。缓冲区是临界资源,各进程必须互斥地访问。

在这里插入图片描述

1.2 问题分析

  1. 关系分析。生产者和消费者对缓冲区互斥访问时互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产后,消费者才能消费,它们时互斥关系
  2. 整理思路。只有生产者和消费者两个进程,正好是这两个进程存在着互斥和同步关系。那么需要解决的是互斥和同步PV操作的位置
  3. 信号量设置。信号量mutex作为互斥信号量,用于控制互斥访问缓冲区,互斥信号量初值为1;信号量full表示缓冲区有多少物品,初值为0。信号量empty表示缓冲区有多少空位置,初值为n。注意细节:往往分析问题时只考虑到full,未考虑到empty,这是不够的,full与empty是一对相辅相成的信号量。

同步抽象:
①缓冲区没满 -> 生产者生产
②缓冲区没空 -> 消费者消费

互斥关系:缓冲区是临界资源,各进程必须互斥地访问
在这里插入图片描述

1.3 解决问题

semaphore mutex=1;							// 临界区互斥信号量 
semaphore empty=n;							// 空闲缓冲区 
semaphore full=0;							// 缓冲区初始化为空 
producer(){									// 生产者进程 while(1){生产一个产品							// 生产数据 P(empty); (生产者生产要空间)			// 获取空闲空间 P(mutex); (互斥夹紧)					// 进入临界区 将产品放入缓冲区 (临界区行为)			// 将生产的数据放入缓冲区 V(mutex); (互斥夹紧)					// 离开临界区,释放互斥信号量 V(full); (生产者生产完后产生产品) 		// 对应产品数加1 }
}
consumer(){									// 消费者进程 while(1){						P(full);							// 获取物品 P(mutex);							// 进入临界区 从缓冲区取出一个产品					// 从缓冲区取数据 V(mutex);							// 离开临界区,释放互斥信号量V(empty);							// 对应空闲空间加1 使用产品 }
} 

在这里插入图片描述

1.4 问题扩展思考

问:能否改变相邻P、V操作的顺序

在这里插入图片描述

讨论:
若此时缓冲区内已经放满产品,则 empty=0,full=n。
则生产者进程执行① 使mutex变为0,再执行②,由于已没有空闲缓冲区,因此生产者被阻塞。
由于生产者阻塞,因此切换回消费者进程。消费者进程执行③,由于mutex为0,即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞。
这就造成了生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”。
同样的,若缓冲区中没有产品,即full=0,empty=n。按③④① 的顺序执行就会发生死锁。
因此,实现互斥的P操作一定要在实现同步的P操作之后。
V操作不会导致进程阻塞,因此两个V操作顺序可以交换。

答:实现互斥的P操作一定要在实现同步的P操作之后,这个顺序是不容更改的,但退出临界区后的V操作就没有那么严格的限制。同时“使用产品”这一行为是可以放入临界区中取执行,但这样做会使临界区变长,CPU执行临界区耗费时间更多,而临界区的执行又是一气呵成的,执行临界区的时间变长意味着系统的吞吐量会降低,导致并发度降低。

1.5 总结

在这里插入图片描述

2. 多生产者-多消费者问题

2.1 问题描述

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用PV操作实现上述过程。

在这里插入图片描述

2.2 问题分析

  1. 关系分析。由每次只能向其中放入一只水果可知,爸爸和妈妈是互斥关系。爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行,不可能并发。
  2. 整理思路。这里有4个进程,实际上可抽象为两个生产者和两个消费者被连接到大小为1的缓冲区上。
  3. 信号量设置。信号量plate设置为互斥信号量,表示是否允许向盘子放入水果,初值为1,1表示允许放入,且只允许放入一个。信号量apple表示盘子中是否有苹果,初值为0表示无苹果,不许取,apple=1表示可以取。信号量orange表示盘子中是否有橘子,初值为0表示无橘子,不许取,orange=1表示可以取。

同步抽象:
①父亲将苹果放入盘子后,女儿才能取苹果
②母亲将橘子放入盘子后,儿子才能取橘子
③只有盘子为空时,父亲或母亲才能放入水果

互斥关系:对缓冲区(盘子)的访问要互斥地进行
在这里插入图片描述

2.3 解决问题

semaphore plate=1,apple=0,orange=0;
dad(){							// 父亲进程 while(1){准备一个苹果;		P(plate);				// 消费盘子 P(mutex);					把苹果放入盘子V(mutex);					 V(apple); 				// 生产苹果 }
} 
mom(){							// 母亲进程 while(1){准备一个橘子;P(plate);				// 消费盘子 P(mutex);把橘子放入盘子;V(mutex);V(orange); 				// 生产橘子 }
} 
daughter(){						// 女儿进程 while(1){P(apple);				// 消费苹果 P(mutex);从盘中取出苹果;V(mutex);V(plate);				// 提供盘子 吃掉苹果; }
} 
son(){							// 儿子进程 while(1){P(orange);				// 消费橘子 P(mutex);从盘中取橘子;V(mutex);V(plate);				// 提供盘子 吃掉橘子 }
} 

2.4 问题扩展思考

问:可不可以不用互斥信号量

讨论:本题中的缓冲区大小为1,在任何时刻,apple、orange、plate 三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区

答:即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象

结论:当缓冲区大小为1时,可以不设置一个互斥信号量mutex,因为缓冲区本身也充当了一个锁,当缓冲区大小大于1时,必须设置一个互斥信号量mutex。总之,不管三七二十一,设置一个互斥信号量准没错

实现不用互斥信号量mutex的方法,就是将关于mutex的PV操作去掉即可。

2.5 总结

在这里插入图片描述

3. 吸烟者问题(生产者-消费者问题的推广)

3.1 问题描述

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

在这里插入图片描述

3.2 问题分析

  1. 关系分析。供应者与三个抽烟者分别是同步关系。由于供应者无法同时满足两个或两个以上的抽烟者,三个抽烟者对抽烟这个动作互斥。
  2. 整理思路。显然这里有4个进程。供应者作为生产者向三个抽烟者提供材料。
  3. 信号量设置。信号量offer1,offer2,offer3分别表示烟草和纸,烟草和胶水,纸和胶水组合的资源。信号量finish用于互斥进行抽烟动作。

同步抽象:
①桌上有组合一 -> 第一个抽烟者取走东西
②桌上有组合二 -> 第二个抽烟者取走东西
③桌上有组合三 -> 第三个抽烟者取走东西
④发出完成信号 -> 供应者将下一个组合放到桌上

互斥关系:对桌子的访问需互斥的进行,桌子抽象为容量为1的缓冲区
在这里插入图片描述

3.3 解决问题

int num=0;
semaphore offer1=0,offer2=0,offer3=0;
process provider(){			// 供应者进程 while(1){num++;num=num%3;if(num==0){将组合一放在桌上; V(offer1);}else if(num==1){将组合二放在桌上; V(offer2);}else{将组合三放在桌上; V(offer3);}P(finish);}
}
smoker1(){					// 吸烟者1进程 while(1){P(offer1);从桌上拿走组合一,卷烟并抽掉;V(finish); }
} 
smoker2(){					// 吸烟者1进程 while(1){P(offer2);从桌上拿走组合二,卷烟并抽掉;V(finish); }
} 
smoker3(){					// 吸烟者1进程 while(1){	P(offer3);从桌上拿走组合三,卷烟并抽掉;V(finish); }
} 

3.4 总结

在这里插入图片描述

4. 读者写者问题

4.1 问题描述

有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出

在这里插入图片描述

4.2 问题分析

  1. 关系分析。由题目分析读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥关系。
  2. 整理思路。两个进程,即读者和写者。写者和任意进程互斥,用互斥信号量的PV操作即可解决。而读者必须在实现与写者互斥的同时,实现与其他读者的同步。因此简单的一对PV操作是无法解决问题的。这里采用一个计数器,用它来判断当前是否有读者读文件。当有读者时,写者是无法写文件的,此时读者会一直占用文件,当没有读者时,写者才可以写文件。同时,这里不同读者对计数器的访问也应该是互斥的。即计数器也是种互斥资源,对其的修改要一气呵成。
  3. 信号量设置。首先设置信号量count为计数器,用于记录当前读者的数量,初值为0;设置mutex为互斥信号量,用于更新count变量时的互斥;设置互斥信号量rw,用于保证读者和写者的互斥访问

同步抽象: 读者与读者间可以同时读文件,通过count信号量来确定第一个读者和最后一个读者

互斥关系:
①:读者与写者互斥
②:写者与写者互斥
③:读者间对于count信号量的访问互斥

4.3 解决问题

int count=0;
semaphore rw=1,mutex=1;
writer(){				// 写者进程 while(1){P(rw);			// 写之前加锁 写文件...V(rw); 			// 写之后解锁 }
} 
reader(){				// 读者进程 while(1){P(mutex);		// 互斥访问count,对count的修改要一气呵成 if(count==0)	// 只有第一个读者才需要对写操作进行加锁 P(rw);count++;V(mutex);读文件...P(mutex);		count--;		// 读完文件需要count减1,表示当前读进程数减1 if(count==0)	// 最后一个读者才需要对写操作进行解锁 V(rw);V(mutex); }
}

4.4 问题扩展思考

①问:为什么读者进程非得需要一个计数器count呢?

将count信号与对count互斥访问的mutex信号去掉,如下所示:

semaphore rw=1; 
writer(){				// 写者进程 while(1){P(rw);			写文件...V(rw); 			}
} 
reader(){				// 读者进程 while(1){P(rw);读文件...V(rw);}
}

答:分析来看,此时读者与写者依旧是互斥的,但读者和读者之间居然也变为了互斥的了,当存在一个读者进程在读文件时,此时另一个读者进程到来,会被阻塞在P(rw)处。有人说,那直接修改rw的值为2不就行了吗,显然这是个蠢方法,rw=2,先不说最多只能容忍2个读者进程进行读文件,如果此时来了个写者进程,那么读者与写者一同进入临界区,读者与写者的互斥就被破坏了。同理,将rw信号量拆分为r和w两个信号量想要进行读者与写者的互斥以及读者与读者的同时进行也是不能够实现的。

②问:不对count的修改做到一气呵成会出现什么情况?

将对count互斥访问的mutex信号去掉,如下所示:

int count=0;
semaphore rw=1;
writer(){				// 写者进程 while(1){P(rw);			 写文件...V(rw); 			}
} 
reader(){				// 读者进程 while(1){if(count==0)P(rw);		②count++;读文件...count--;		 if(count==0)	 V(rw);}
}

答:分析来看,若两个读进程并发执行,则 count=0时两个进程也许都能满足 if 条件,都会执行P(rw),从而使第二个读进程阻塞的情况。即读者1先执行①,判断成立进入判断后执行P(rw),切换至读者2,此时读者2也先执行①,判断成立进入判断执行P(rw)时,就会被阻塞在②处,从而造成读者与读者之间居然变成了互斥访问文件了。

③问:其实上述正确解答是存在一个问题的,你能否发现并解决?

答:分析来看,存在的潜在问题是,只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”。因此,这种算法中,读进程是优先的。解决,新增一个信号量w,并在上述程序进程中各增加一对PV操作,就可以解决这个问题,具体如下描述。

int count=0;
semaphore rw=1,mutex=1,w=1;
writer(){				// 写者进程 while(1){P(w);			// 在无写进程请求时进入 P(rw);			// 写之前加锁 写文件...V(rw); 			// 写之后解锁 V(w);			// 恢复对共享文件的访问 }
} 
reader(){				// 读者进程 while(1){P(w); 			// 在无写进程请求时进入  P(mutex);		// 互斥访问count,对count的修改要一气呵成 if(count==0)	// 只有第一个读者才需要对写操作进行加锁 P(rw);count++;V(mutex);V(w);			// 恢复对共享文件的访问 读文件...P(mutex);		count--;		// 读完文件需要count减1,表示当前读进程数减1 if(count==0)	// 最后一个读者才需要对写操作进行解锁 V(rw);V(mutex); }
}

分析以下并发执行 P(w) 的情况:
读者 -> 读者2
写者 -> 写者2
写者 -> 读者1
读者 -> 写者 -> 读者2
写者 -> 读者 -> 写者2

结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,而是相对公平的先来先服务原则。有的书上把这种算法称为“读写公平法”。

4.5 总结

在这里插入图片描述

5. 哲学家进餐问题

5.1 问题描述

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

在这里插入图片描述

5.2 问题分析

  1. 关系分析。5名哲学家与左右邻居对其中筷子的访问时互斥关系。
  2. 整理思路。本题的关键时如何让一名哲学家拿到左右两根筷子而不造成死锁或饥饿现象。解决办法有两个:一是让他们同时拿两根筷子;二是对每名哲学家的动作制定规则,避免饥饿或死锁现象的发生。
  3. 信号量设置。定义互斥信号量chopstick[5] ={1,1,1,1,1,},用于对5个筷子的互斥访问。哲学家按顺序编号为0~4,哲学家i左边筷子的编号为i,哲学家右边筷子的编号为(i+1)%5。

如果只是按这种方式来设置信号量然后实现程序,是会有问题的,后续5.3节再展开讨论

5.3 解决问题

semaphore chopstick[5]={1,1,1,1,1};
Pi(){							// i号哲学家进程 do{P(chopstick[i]);		// 取左边筷子 P(chopstick[(i+1)%5]);	// 取右边筷子 就餐;V(chopstick[i]);		// 放回左边筷子 V(chopstick[(i+1)%5]);	// 放回右边筷子 思考; }while(1);
}

这种实现存在以下问题:
当5名哲学家都想要进餐并分别拿起左边的筷子时(都恰好执行完P(chopstick[i]);)筷子已被拿光,等到他们再想拿右边的筷子时(执行P(chopstick[(i+1)%5]);)就全被阻塞,因此出现了死锁。
在这里插入图片描述
解决方法:

①:至多允许4名哲学家同时进餐,如下描述:(破坏循环等待)

semaphore chopstick[5]={1,1,1,1,1};
semaphore count=4;				// 至多允许4名哲学家进餐 
Pi(){							// i号哲学家进程 do{P(count); P(chopstick[i]);		// 取左边筷子 P(chopstick[(i+1)%5]);	// 取右边筷子 就餐;V(chopstick[i]);		// 放回左边筷子 V(chopstick[(i+1)%5]);	// 放回右边筷子 V(count);思考; }while(1);
}

②:仅当一名哲学家左右两边筷子都可用时,才允许他抓起筷子(设置mutex信号量,对抓左右两只筷子一气呵成),如下描述:(破坏请求并保持条件)

semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex=1;				// 实现抓筷子一气呵成 
Pi(){							// i号哲学家进程 do{P(mutex); 				// 对抓筷子一气呵成 P(chopstick[i]);		// 取左边筷子 P(chopstick[(i+1)%5]);	// 取右边筷子 V(mutex);				就餐;V(chopstick[i]);		// 放回左边筷子 V(chopstick[(i+1)%5]);	// 放回右边筷子 思考; }while(1);
}

准确来说,这种方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子。各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。

③:对哲学家顺序编号,要求奇数号哲学家先拿左边的筷子,然后拿右边的筷子,而偶数号哲学家则刚好相反,如下描述:(破坏循环等待)

semaphore chopstick[5]={1,1,1,1,1};
Pi(){								// i号哲学家进程 do{if(i%2==0)					// 偶数哲学家,先右后左 {P(chopstick[(i+1)%5]);	// 取右边筷子P(chopstick[i]);		// 取左边筷子 就餐;V(chopstick[(i+1)%5]);	// 放回右边筷子 V(chopstick[i]);		// 放回左边筷子 }else						// 奇数哲学家,先左后右 {P(chopstick[i]);		// 取左边筷子 P(chopstick[(i+1)%5]);	// 取右边筷子就餐;V(chopstick[i]);		// 放回左边筷子V(chopstick[(i+1)%5]);	// 放回右边筷子 }	思考; }while(1);
}

④:AND型信号量

首先,我们需要理解AND信号量的工作原理。AND信号量要求只有当所有关联的信号量都满足条件(即非负)时,才允许一个进程或线程继续执行。
即,为了解决这个问题,我们可以为每个哲学家设置一个AND信号量。初始时,所有信号量的值都为1,表示每只筷子都可用。当一个哲学家想要进餐时,他需要同时获取两只筷子。这可以通过使用AND信号量来实现,只有当两只筷子的信号量都为1时,该哲学家才能继续执行。
通过使用AND信号量,我们可以确保在任何时候只有一个哲学家能够拿起两只筷子,从而避免死锁的发生。
如下描述:

semaphore chopstick[5]={1,1,1,1,1};
do{Swait(chopstick[i],chopstick[(i+1)%5]); 就餐; Ssignal(chopstick[i],chopstick[(i+1)%5]); 思考; 
}while(1);

5.4 总结

在这里插入图片描述

6. 管程

在这里插入图片描述

6.1 为什么要引入管程

在这里插入图片描述

6.2 管程的定义和基本特征

在这里插入图片描述

6.3 扩展1

在这里插入图片描述
在这里插入图片描述

6.4 扩展2

在这里插入图片描述

6.5 总结

在这里插入图片描述

相关文章:

  • 高功率激光输出稳定性不足?OAS 光学软件来攻克
  • 【Python网络爬虫实战指南】从数据采集到反反爬策略
  • ActiveMQ 快速上手:安装配置与基础通信实践(一)
  • HTB - BigBang靶机记录
  • 【MySQL数据库】表的增删改查
  • 雪花算法生成int64,在前端js的精度问题
  • PostgreSQL的dblink扩展模块使用方法
  • Java并发编程|CompletableFuture原理与实战:从链式操作到异步编排
  • 数据库监控 | MongoDB监控全解析
  • vue3实现v-directive;vue3实现v-指令;v-directive不触发
  • 【AI平台】n8n入门1:详细介绍n8n的多种安装方式(含docer图形化安装n8n)
  • 武汉火影数字虚拟展厅制作:打破时空限制的数字化盛宴
  • 【高频考点精讲】JavaScript中的组合模式:从树形结构到组件嵌套实战
  • 基于 Spring Boot 的银行柜台管理系统设计与实现(源码+文档+部署讲解)
  • AD16如何设置合适的PCB板框
  • 常见的限流算法
  • 两段文本比对,高亮出差异部分
  • 最新AI-Python机器学习与深度学习技术在植被参数反演中的核心技术应用
  • Redis从入门到上手-全面讲解redis使用.
  • Vue3中index.html与app.vue、main.ts三个文件的作用和关系
  • 刘非任中共浙江省委常委、杭州市委书记
  • 马上评|起名“朱雀玄武敕令”?姓名权别滥用
  • 石磊当选河北秦皇岛市市长
  • 华夏银行青岛分行另类处置不良债权,德州近百亩土地被神奇操作抵押贷款
  • 最高法:“盗链”属于信息网络传播行为,构成侵犯著作权罪
  • 天问三号计划2028年前后发射实施,开放20千克质量资源