操作系统经典同步问题

一、同步与互斥

1.1 同步

这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。实质就是同步

1.2 互斥

互斥也称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一个进程才允许访问此临界资源

例:

P(S);
访问这种资源;
V(S)

1.3 同步机制应遵循准则

  • 空闲等待
  • 忙则等待
  • 有限等待
  • 让权等待

二、互斥的基本方法

二、生产者——消费者问题

2.1 单一生产者——消费者问题

2.1.1 问题描述

系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者
进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)

生产者、消费者共享一个初始为空、大小为n的缓冲区。

只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。

只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。缓冲区没空→消费者消费

核心思想:先看看有没有我需要的,再决定进去

2.1.2 关系分析

生产者P一个空闲缓冲区,V一个产品。

消费者P一个产品,V一个空闲缓冲区。

mutex是一个互斥信号量,实现对缓冲区的互斥访问。

empty和full是同步信号量,empty表示空闲缓冲区数量,full表示产品数量。

对于producer,生产一个产品要P(empty),表示用了一个空闲缓冲区,生产完即V(full),产生了一个产品。

对于consumer,消耗一个产品P(full)后就释放了一个缓冲区V(empty)。

互斥的是缓冲区,同步的是空闲缓冲区资源和产品。

2.1.3 伪代码描述

semaphore mutex=1;//互斥信号量,实现对缓冲区的互斥访问
semaphore empty=n;//同步信号量,表示空闲缓冲区的数量
semaphore full=0;//同步信号量,表示产品的数量,也即非空缓冲区的数量,初始缓冲区为0
//empty + full =n
producer() {
	while(1){//while(true)
		生产一个产品;
		P(empty);
		P(mutex);
		放入;
		V(mutex);
		V(full);
	}
}
consumer() {
	while(1){
		P(full);
		P(mutex);
		取一个产品;
		V(mutex);
		V(empty);
		消耗产品;
	}
}

2.2 多生产者——消费者问题

2.2.1 问题描述

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

2.2.2 关系分析

互斥关系:(mutex=1)对缓冲区(盘子)的访问要互斥地进行

同步关系(一前一后):

  1. 父亲将苹果放入盘子后,女儿才能取苹果
  2. 母亲将橘子放入盘子后,儿子才能取橘子
  3. 只有盘子为空时,父亲或母亲才能放入水果

2.2.3 伪代码描述

 semaphore plate=1;//盘子中还可以放多少个水果
 semaphore apple=0;//盘子中有几个苹果
 semaphore orange=0;//盘中有几个橘子
 dad() {
 	while(1){
 		准备一个苹果;
 		P(plate);
 		放入苹果;
 		V(apple);
 	}
 }
 daughter() {
 	while(1){
 		P(apple);
 		取走苹果;
 		V(plate);
 		开炫;
 	}
 }
 mom() {
 	while(1){
 		准备一个橘子;
 		P(plate);
 		放入橘子;
 		V(orange);
 	}
 }
 son() {
 	while(1){
 		P(orange);
 		取走橘子;
 		V(plate);
 		开炫;
 	}
 }

当然,多生产者——消费者问题如果与单一生产者——消费者问题的信号量保持一致设置也可以,如下:

semaphore mutex=1;//互斥信号量,实现对缓冲区的互斥访问
semaphore empty=1;//同步信号量,表示空闲缓冲区的数量其实就是上面的plate
semaphore apple=0;//盘子中有几个苹果
semaphore orange=0;//盘中有几个橘子
 dad() {
 	while(1){
 		准备一个苹果;
 		P(empty);
 		P(mutex);
 		放入苹果;
 		V(mutex);
 		V(apple);
 	}
 }
 ......

可以看出来,semaphore mutex=1;其实并没有用,这是因为:缓冲区大小为1,在任何时刻,apple、orange、plate三个同步信号量中最多只有一个是1。因此在任何时刻最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区…

但是当缓冲区大小大于1的时候,父亲P(plate),可以访问盘子→母亲P(plate),可以访问盘子→父亲在往盘子里放苹果,同时母亲也可以往盘子里放橘子,于是就出现了两个进程同时访问缓冲区的情况可能导致两个进程写入缓冲区的数据相互覆盖的情况。因此,如果缓冲区大小大于1,就必须专门设置一序信号量mutex长保证互斥访问缓冲区。

三、读者——写者问题

3.1 问题描述

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

3.2 关系分析

两类进程:写进程、读进程

互斥关系:写进程一写进程、写进程一读进程。读进程与读进程不存在互斥问题。

3.1.1 读优先(可能写进程饥饿)

关键点就在于需要对第一个进入的读进程P操作,而对最后一个退出的读进程V操作,以及对计数器的作用思考。

伪代码描述

int readcount=0;//读进程数量
semaphore rwmutex=1;//读写进程文件的互斥访问
semaphore rmutex=1;//对readcount变量的互斥访问
writer() {
	while(1){
		P(rwmutex);
		写文件;
		V(rwmutex);
	}
}
reader() {
	while(1){
		P(rmutex);//进入时对readcount互斥更新
		if(readcount==0) {
			P(rwmutex);//第一个进来的读进程负责对文件上锁
		}
		readcount++;
		V(rmutex);
		读文件;
		P(rmutex);//读完之后退出,不要忘了对readcount的更新
		readcount--;
		if(readcount==0) {
			V(rwmutex);//最后一个退出的读进程负责释放文件
		}
		V(rmutex);
	}
}

3.1.2 读写公平

相比于上述读优先的情况,写进程可能阻塞的原因是没有实现读进程和写进程的公平排队

伪代码描述

int readcount=0;//读进程数量
semaphore rwmutex=1;//读写进程文件的互斥访问
semaphore rmutex=1;//对readcount变量的互斥访问
semaphore S=1;//排队
writer() {
	while(1){
		P(S);
		P(rwmutex);
		写文件;
		V(rwmutex);
		V(S);
	}
}
reader() {
	while(1){
		P(S);
		P(rmutex);//进入时对readcount互斥更新
		if(readcount==0) {
			P(rwmutex);//第一个进来的读进程负责对文件上锁
		}
		readcount++;
		V(rmutex);
		//V(S);
		读文件;
		P(rmutex);//读完之后退出,不要忘了对readcount的更新
		readcount--;
		if(readcount==0) {
			V(rwmutex);//最后一个退出的读进程负责释放文件
		}
		V(rmutex);
		V(S);
	}
}

3.1.3 写优先

核心思想:写进程排队的时候把事情办了

int readcount=0;//读进程数量
int writecount=0;//写进程数量
semaphore rwmutex=1;//读写进程文件的互斥访问
semaphore rmutex=1;//对readcount变量的互斥访问
semaphore wmutex=1;//对writecount变量的互斥访问
semaphore S=1;//排队
reader() {
	while(1){
		P(S);
		P(rmutex);//进入时对readcount互斥更新
		if(readcount==0) {
			P(rwmutex);//第一个进来的读进程负责对文件上锁
		}
		readcount++;
		V(rmutex);
		读文件;
		P(rmutex);//读完之后退出,不要忘了对readcount的更新
		readcount--;
		if(readcount==0) {
			V(rwmutex);//最后一个退出的读进程负责释放文件
		}
		V(rmutex);
		V(S);
	}
}
writer() {
	while(1){
		P(wmutex);
		if(writecount==0) {
			P(S);
		}
		writecount++;
		V(wmutex);
		P(rwmutex);
		写文件;
		V(rwmutex);
		P(wmutex);
		writecount--;
		if(writecount==0) {
			V(S);
		}
		V(wmutex);
	}
}

四、吸烟者问题

4.1 问题描述

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

4.2 问题分析

桌子可以抽象为容量为1的缓冲区要互斥访问

组合一:纸+胶水

组合二:烟草+胶水

组合三:烟草+纸

同步关系(从事件的角度来分析):

桌上有组合一→第一个抽烟者取走东西

桌上有组合二→第二个抽烟者取走东西

桌上有组合三→第三个抽烟者取走东西

发出完成信号→供应者将下一个组合放到桌上

当然这个和上面讲过多生产者——消费者问题的一样,也不需要设置一个单独的信号量进行互斥访问,因为缓冲区大小为1,同一时刻,四个同步信号量中至多有一个的值为1

4.3 伪代码

semaphore offerl = 0;//桌上组合一的数量
semaphore offer2 = 0;//桌上组合二的数量
semaphore offer3 = 0;//桌上组合三的数量
semaphore finish = 0;//同步,抽烟是否完成
int i = 0;//三个抽烟者轮流抽烟
provider() {
	while(1){
		if(i==0){
		将组合一放桌上;
		V(offer1);
		} else if(i==1){
			将组合二放桌上;
			V(offer2);
		}else if (i== 2) {
			将组合三放桌上;
			V(offer3);
			i=(i+1)%3;
			P(finish);
	}
}
smoker1() {
	while(1){
		P(offer1);
		从桌上拿走组合一;
		卷烟;
		抽掉;
		V(finish);
	}
}
smoker2() {
	while(1){
		P(offer2);
		从桌上拿走组合二;
		卷烟;
		抽掉;
		V(finish);
	}
}
smoker3() {
	while(1){
		P(offer3);
		从桌上拿走组合三;
		卷烟;
		抽掉;
		V(finish);
	}
}

五、哲学家进餐问题

5.1 问题描述

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

5.2 关系分析

如果5个哲学家并发地拿起了自己左手边的筷子,那么每位哲学家就要循环等待右边的人放下筷子(阻塞)。就会发生“死锁”

所以,如何防止死锁的发生呢?

①可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的

②要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只镜子,另另一会直接阻寒。这就避免了占有一支后再等待另一只的情况。

③仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。

所以,各哲学家拿筷子这件事必须互斥的执行。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。

5.3 伪代码实现

semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex=1;//互斥地取筷子
Pi() { //i号哲学家的进程
	while(1){
		P(mutex);
		P(chopstick[i]);//拿左
		P(chopstick[(i+1)%5]);//拿右
		V(mutex);
		吃饭...
		V(chopstick[i]);//放左
		V(chopstick[(i+1)85]);//放右
		思考...
}

5.4 B站大佬的一个模板

第一步
while(1) {
	P(Lock);
	if(资源够){
		值--;
		取资源;
		V(Lock);
		break;
	}
	V(Lock);
}
第二步
eat
第三步
一口气还所有
P
还
V

六、理发师问题

叫号,提供服务与被服务关系,不产生其资源

使用一变量描述等待的人数+service=0来表示有无服务+可能有的互斥

相当于顾客生产“顾客资源”,消耗“服务资源”

服务员消耗“顾客资源”,生产“服务资源”

waiting=0;//等待服务的人    mutex

但其实,这个理发师问题也可以转换为生产者——消费者问题进行常规分析,求解。

09-17 08:24