信号的同步互斥——五个经典问题

单生产者和单消费者问题

描述:
有两个进程:一组生产者进程和一组消费者进程共享一个初始为空、固定大小为n的缓存(缓冲区)。生产者的工作是制造一段数据,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待,如此反复; 同时,只有缓冲区不空时,消费者才能从中取出消息,一次消费一段数据(即将其从缓存中移出),否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。

问题的核心是:

  1. 要保证不让生产者在缓存还是满的时候仍然要向内写数据;
  2. 不让消费者试图从空的缓存中取出数据。

生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。

解决思路:对于生产者,如果缓存是满的就去睡觉。消费者从缓存中取走数据后就叫醒生产者,让它再次将缓存填满。若消费者发现缓存是空的,就去睡觉了。下一轮中生产者将数据写入后就叫醒消费者。
不完善的解决方案会造成“死锁”,即两个进程都在“睡觉”等着对方来“唤醒”。

只有生产者和消费者两个进程,正好是这两个进程存在着互斥关系和同步关系。那么需要解决的是互斥和同步PV操作的位置。使用“进程间通信”,“信号标”semaphore就可以解决唤醒的问题:

方法
我们使用了两个信号标:full 和 empty 。信号量mutex作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为 1;信号量 full 用于记录当前缓冲池中“满”缓冲区数,初值为0。信号量 empty 用于记录当前缓冲池中“空”缓冲区数,初值为n。新的数据添加到缓存中后,full 在增加,而 empty 则减少。如果生产者试图在 empty 为0时减少其值,生产者就会被“催眠”。下一轮中有数据被消费掉时,empty就会增加,生产者就会被“唤醒”。

伪代码

semaphore mutex=1; //临界区互斥信号量
semaphore empty=n;  //空闲缓冲区
semaphore full=0;  //缓冲区初始化为空
producer ()//生产者进程 
{
    while(1)
    {
        produce an item in nextp;  //生产数据
        P(empty);  //获取空缓冲区单元
        P(mutex);  //进入临界区.
        add nextp to buffer;  //将数据放入缓冲区
        V(mutex);  //离开临界区,释放互斥信号量
        V(full);  //满缓冲区数加1
    }
}

consumer ()//消费者进程
{
    while(1)
    {
        P(full);  //获取满缓冲区单元
        P(mutex);  // 进入临界区
        remove an item from buffer;  //从缓冲区中取出数据
        V (mutex);  //离开临界区,释放互斥信号量
        V (empty) ;  //空缓冲区数加1
        consume the item;  //消费数据
    }
}

多生产者和多消费者问题

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

问题分析
(1)关系分析。这里的关系稍复杂一些,首先由每次只能向其中放入一只水果可知爸爸和妈妈是互斥关系。爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行,不可能并发。

(2)整理思路。这里有4个进程,实际上可以抽象为两个生产者和两个消费者被连接到大小为1的缓冲区上。

进程之间的关系:

信号的同步互斥——五个经典问题_第1张图片
(3)信号量设置。首先设置信号量plate为互斥信号量,表示是否允许向盘子放入水果,初值为1,表示允许放入,且只允许放入一个。信号量 apple表示盘子中是否有苹果,初值为0,表示盘子为空,不许取,若apple=l可以取。信号量orange表示盘子中是否有橘子,初值为0,表示盘子为空,不许取,若orange=l可以取。

伪代码

semaphore plate=l, apple=0, orange=0;
dad() {  //父亲进程
    while (1) {
        prepare an apple;
        P(plate) ;  //互斥向盘中取、放水果
        put the apple on the plate;  //向盘中放苹果
        V(apple);  //允许取苹果
    }
}
 
mom() {  // 母亲进程
    while(1) {
        prepare an orange;
        P(plate);  //互斥向盘中取、放水果
        put the orange on the plate;  //向盘中放橘子
        V(orange); //允许取橘子
    }
}
 
son(){  //儿子进程
    while(1){
        P(orange) ;  //互斥向盘中取橘子
        take an orange from the plate;
        V(plate);  //允许向盘中取、放水果
        eat the orange;
    }
}
 
daughter () {  //女儿进程
    while(1) {
        P(apple);  // 互斥向盘中取苹果
        take an apple from the plate;
        V(plate);  //运行向盘中取、放水果
        eat the apple;
    }
   }

吸烟者问题

问题描述
某系统有三个吸烟者进程和一个经销商进程:

  • 每个吸烟者连续不断做烟卷并抽他做好的烟卷,做一支烟卷需要烟草、纸、火柴三种原料,这3个吸烟者分别掌握有烟草、纸和火柴;
  • 经销商源源不断地提供上述三种原料,但他只将其中的两种原料放在桌上,具有另一种原料的吸烟者就可做烟卷并抽烟,且在做完后给经销商发信号,然后经销商再拿出两种原料放在桌上,如此反复。
  • 试设计一个同步算法来描述他们的活动。

分析
吸烟者问题是经典的进程同步问题,其特点在于信号量的设置,本题的关键问题是判断有几个临界资源。烟草、纸和火柴三种原料并不能简单地看成是三种临界资源,因为它们并不是以单独的形式被三个吸烟者进程所竞争,而是以固定的组合被三个进程所申请因此可以考虑如下设置:

  • 三个信号量t1、t2和t3分别代表三种原料组合,即t1表示烟草和纸的组合信号量,t2表示烟草和火柴的组合信号量,t3表示纸和火柴的组合信号量,初值均为0。
  • 经销商一次只能提供一种组合,可以看作是放一个产品的缓冲区,资源量为一个组合,于是我们设置缓冲区的资源信号量为s,初值为1。由于该资源量为1,故可以同时作为互斥量来使用,可以省略对缓冲区操作的互斥信号量。
    信号的同步互斥——五个经典问题_第2张图片
    伪代码
int random; //存储随机数
semaphore offer1=0; //定义信号量对应烟草和纸组合的资源
semaphore offer2=0; //定义信号量对应烟草和胶水组合的资源
semaphore offer3=0; //定义信号量对应纸和胶水组合的资源
semaphore finish=0; //定义信号量表示抽烟是否完成
 
//供应者
while(1){
    random = 任意一个整数随机数;
    random=random% 3;
    if(random==0)
        V(offerl) ; //提供烟草和纸
    else if(random==l) 
        V(offer2);  //提供烟草和胶水
    else
        V(offer3)  //提供纸和胶水
    // 任意两种材料放在桌子上;
    P(finish);
}
 
//拥有烟草者
while(1){
    P (offer3);
    // 拿纸和胶水,卷成烟,抽掉;
    V(finish);
}
 
//拥有纸者
while(1){
    P(offer2);
    // 烟草和胶水,卷成烟,抽掉;
    V(finish);
}
 
//拥有胶水者
while(1){
    P(offer1);
    // 拿烟草和纸,卷成烟,抽掉;
    v(finish);
}

读者写者问题

问题描述
一个数据文件或记录可被多个进程共享。

  • 只要求读文件的进程称为“Reader进程”,其它进程则称为“Writer进程”。

  • 允许多个进程同时读一个共享对象,但不允许一个Writer进程和其他Reader进程或Writer进程同时访问共享对象

“读者—写者问题”是保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。

信号的同步互斥——五个经典问题_第3张图片
读者—写者问题要解决:读、读共享;写、写互斥;写、读互斥

①定义互斥信号量wmutex,实现写、写互斥和写、读互斥。

②定义整型变量Readcount表示正在读的进程数目。由于只要有一个Reader进程在读,便不允许Writer进程写,因此仅当Readcount=0,即无Reader进程在读时,Reader才需要执行Wait(wmutex)操作。若Wait(wmutex)操作成功,Reader进程便可去读,相应地,做Readcount+1操作。同理,仅当Reader进程在执行了Readcount减1操作后其值为0时,才需执行signal(wmutex)操作,以便让Write进程写。

③由于Readcount为多个读进程共享(修改),因此需要以互斥方式访问,为此,需要定义互斥信号量rmutex,保证读进程间互斥访问Readcount。

伪代码:(读者优先)

semaphore wmutex = 1;
semaphore rmutex = 1;
int readcount = 0;// 记录有几个读者
void reader()
{
	while(true)
	{
		wait(rmutex); // rmutex是为了保护readcount共享变量
		if(readcount == 0)
		{
			wait(wmutex);// 保证第一个读进程来,写进程就不能进行
		}
		readcount++;
		signal(rmutex);
		// 读的过程
		wait(rmutex);
		readcount--;
		if(readcount == 0)
		{
			signal(wmutex); // 保证最后一个读进程读完,释放wmutex信号量
		}
		signal(rmutex);
	}
}
void writer()
{
	while(true)
	{
		wait(wmutex);
		// 写的过程
		signal(wmutex);
	}
}

伪代码:(写者优先)

semaphore wmutex = 1;
semaphore rmutex = 1;
semaphore  w=1;//用于实现写优先
int readcount = 0;// 记录有几个读者
void reader()
{
	while(true)
	{
		wait(w);//写优先
		wait(rmutex); // rmutex是为了保护readcount共享变量
		if(readcount == 0)
		{
			wait(wmutex);// 保证第一个读进程来,写进程就不能进行
		}
		readcount++;
		signal(rmutex);
		
		signal(w);//写优先
		// 读的过程
		wait(rmutex);
		readcount--;
		if(readcount == 0)
		{
			signal(wmutex); // 保证最后一个读进程读完,释放wmutex信号量
		}
		signal(rmutex);
	}
}
void writer()
{
	while(true)
	{
	    wait(w);//写优先
		wait(wmutex);
		// 写的过程
		signal(wmutex);
		signal(w);//写优先
	}
}

哲学家进餐问题

问题描述
五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在桌子上有五只碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐毕,放下筷子继续思考。

信号的同步互斥——五个经典问题_第4张图片
分析
放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用,为了实现对筷子的互斥访问,可以用一个信号量表示筷子,由这五个信号量构成信号量数组。

伪代码:

semaphore chopstick[5] = {1,1,1,1,1};
while(true)
{
	/*当哲学家饥饿时,总是先拿左边的筷子,再拿右边的筷子*/
	wait(chopstick[i]);
	wait(chopstick[(i+1)%5]);

	// 吃饭
 
	/*当哲学家进餐完成后,总是先放下左边的筷子,再放下右边的筷子*/
	signal(chopstick[i]);
	signal(chopstick[(i+1)%5]);
}

上述的代码可以保证不会有两个相邻的哲学家同时进餐,但却可能引起死锁的情况。假如五位哲学家同时饥饿而都拿起的左边的筷子,就会使五个信号量chopstick都为0,当他们试图去拿右手边的筷子时,都将无筷子而陷入无限期的等待。

为避免死锁,可以使用以下三种策略:

策略一:至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。定义信号量count,只允许4个哲学家同时进餐,这样就能保证至少有一个哲学家可以就餐。

semaphore chopstick[5]={1,1,1,1,1};
semaphore count=4; // 设置一个count,最多有四个哲学家可以进来
void philosopher(int i)
{
	while(true)
	{
		think();
		wait(count); //请求进入房间进餐 当count为0时 不能允许哲学家再进来了
		wait(chopstick[i]); //请求左手边的筷子
		wait(chopstick[(i+1)%5]); //请求右手边的筷子
		eat();
		signal(chopstick[i]); //释放左手边的筷子
		signal(chopstick[(i+1)%5]); //释放右手边的筷子
		signal(count); //离开饭桌释放信号量
	}
}

**策略二:**仅当哲学家的左右两支筷子都可用时,才允许他拿起筷子进餐。可以利用AND 型信号量机制实现,也可以利用信号量的保护机制实现。利用信号量的保护机制实现的思想是通过记录型信号量mutex对取左侧和右侧筷子的操作进行保护,使之成为一个原子操作,这样可以防止死锁的出现。描述如下:

  1. 用记录型信号量实现:
semaphore mutex = 1; // 这个过程需要判断两根筷子是否可用,并保护起来
semaphore chopstick[5]={1,1,1,1,1};
void philosopher(int i)
{
	while(true)
	{
		/* 这个过程中可能只能由一个人在吃饭,效率低下,有五只筷子,其实是可以达到两个人同时吃饭 */
		think();
		wait(mutex); // 保护信号量
		wait(chopstick[(i+1)%5]); // 请求右手边的筷子
		wait(chopstick[i]); // 请求左手边的筷子
		signal(mutex); // 释放保护信号量
		eat();
		signal(chopstick[(i+1)%5]); // 释放右手边的筷子
		signal(chopstick[i]); // 释放左手边的筷子
	}
}

  1. 用AND型信号量实现
semaphore chopstick[5]={1,1,1,1,1};
do{
	//think()
	Swait(chopstick[(i+1)%5],chopstick[i]);
	//eat()
	Ssignal(chopstick[(i+1)%5],chopstick[i]);
}while(true)

**策略三:**规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则先拿起他右边的筷子,然后再去拿他左边的筷子。按此规定,将是1、2号哲学家竞争1号筷子,3、4号哲学家竞争3号筷子。即五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获得两支筷子而进餐。

semaphore chopstick[5]={1,1,1,1,1};
void philosopher(int i)
{
	while(true)
	{
		think();
		if(i%2 == 0) //偶数哲学家,先右后左。
		{
			wait (chopstick[(i + 1)%5]) ;
			wait (chopstick[i]) ;
			eat();
			signal (chopstick[(i + 1)%5]) ;
			signal (chopstick[i]) ;
		}
		else //奇数哲学家,先左后右。
		{
			wait (chopstick[i]) ;
			wait (chopstick[(i + 1)%5]) ;
			eat();
			signal (chopstick[i]) ;
			signal (chopstick[(i + 1)%5]) ;
		}
	}
}

你可能感兴趣的:(操作系统)