四大经典同步问题

1.生产者-消费者问题

1.经典问题

问题描述

一组生产者进程和一组消费者进程共享一个初始为空,大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待,只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息或一个消费者从中取出消息。

问题分析

关系分析

生产者和消费者对缓冲区访问都是互斥访问(生产者之间互斥,避免数据覆盖;消费者之间互斥,避免数据重复消费;生产者和消费者之间互斥)。同时,生产者和消费者之间也是相互协作的关系,只有生产者生产之后,消费者才能消费,属于同步关系。

信号量设置

设置两个同步信号量:full=0,表示初始满缓冲区个数为0个,empty=n,表示初始空缓冲区个数为n个。

设置一个互斥信号量:mutex,初值为1,用于控制对缓冲区的互斥访问。

生产者生产之前判断是否有空余缓冲区,生产一个消息之后,

代码示例

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);
    }
}
consumer(){
    while(1){
        p(full);	//只在有满缓冲区时,才进行下一步
        p(mutex);	//在有满缓冲区的前提下,
        remove an item from buffer;
        v(mutex);
        v(empty);
        consume the item;
    }
}

这种情形,比较简单,找到同步和互斥关系,然后设置好相应的信号量,根据情况赋予初值即可(互斥信号量初值肯定为1)。每次申请资源前,执行p操作,每次释放资源后,执行v操作。

2.复杂生产者消费者问题

问题描述

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

问题分析

关系分析

每次只能向盘子中放入一个水果,爸爸和妈妈是互斥关系。爸爸和女儿,妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥或同步关系,因为他们是选择条件执行,不可能并发。

信号量设置

设置两个同步信号量:apple=0,用于协调爸爸和女儿,orange=0,用于协调妈妈和儿子

设置一个互斥信号量:plate=1,用于保证爸爸和妈妈互斥访问盘子

代码示例

semaphore plate=1,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;
    }
}

这是生产者-消费者模型的升级版,本质上是两组生产者和消费者。问题的关键是生产者之间的互斥,生产者和消费者之间的同步连续执行。

比如爸爸和女儿在生产之后都没有立即释放盘子,而是释放对应的水果,这样儿子和女儿都可以访问盘子,但是只有有相应水果的消费者才可以顺利进行消费,然后释放盘子。

儿子和女儿访问盘子都没有p(plate),因为他们不需要对盘子进行互斥访问,两个消费者是选择条件执行。

找到这个关键点即可。

2.读者-写者问题

问题描述

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

问题分析

关系分析

读者和写者是互斥的,写者和写者也是互斥的,但是读者和读者不存在互斥问题。

写者比较好处理,和任何进程都是互斥的,用互斥信号量的p操作和v操作即可解决。读者的问题比较复杂,需实现与写者互斥的同时,实现与其他读者的同步。

可以引入一个计数器(普通变量,而非记录型信号量,也不是整型信号量,只用于统计读者进程数量),用于记录当前读者的数量,如果读者数为0,则需要判断是否可以互斥访问,如果读者数大于0,则说明当前不存在写者,可以直接访问。

但是对于计数器的修改也需要互斥进行,所以需要额外引入一个互斥信号量,用户保护计数器的修改。

信号量设置

设置两个互斥信号量:

mutex=1用于实现计数器(普通变量)的修改,

rw=1用于实现读者和写者对文件的互斥访问

代码示例

int count=0;
semaphore mutex=1;
semaphore rw=1;
writer(){
   while(1){
       p(rw);
       writing;
       v(rw);
   }
}
reader(){
    while(1){
        p(mutex);	//对计数器的访问进行互斥
        if(count==0)
            p(rw);	//只在第一个读者申请访问时,判断是否可以互斥访问文件,后面的读者直接数目加一
        count++;
        v(mutex);
        reading;
        p(mutex);
        count--;
        if(count==0)
            v(rw);	//只在最后一个读者读完时释放rw锁
        v(mutex);        
    }
}

在上面的算法中,读进程是具有优先权的,只有一直有读进程在,写进程就有可能被饿死。只有读进程都执行完了,写进程才有机会访问文件。

算法改进

当有读者正在访问文件时,如共有写进程请求访问,这时应禁止后续读者的请求,等到已在共享文件的读者执行完毕,立即让写者执行。只有在无写者执行的情况下,才允许读者再次执行。

代码示例

int count=0;
semaphore mutex=1;
semaphore rw=1;
semaphore w=1;
writer(){
    while(1){
        p(w);
        p(rw);
        writing;
        v(rw);
        v(w);
    }
}

reader(){
    while(1){
        p(w);		//只要来了一个写者,后续的读者就会在这里阻塞,当前已经在共享文件执行的进程执行完之后,释放rw,写者执行,然后读者才能继续执行。
        p(mutex);
        if(count==0)
            p(rw);
        count++;
        v(mutex);
        v(w);
        reading;
        p(mutex);
        count--;
        if(count==0)
            v(rw);
        v(mutex);
    }
}

这个算法对于写者来说还是不太公平的,如果共享文件已经有很多读者在访问了,这个时候新来的写者必须等这些读者全都运行完了之后才可以访问共享文件。但是这个算法改进了读者一直占用共享文件的问题,只要写者进程到来,就可以占用w,使得后续的读进程被阻塞。

3.哲学家进餐问题

问题描述

一张圆桌上,5名哲学家,每两名哲学家之间有一根筷子。哲学家需拿起两根筷子才可以进餐,否则等待。

问题分析

关系分析

每名哲学家与左右邻居对筷子的访问是互斥的

思路

有两种解决办法:

1.同时拿起两根筷子

2.对每个哲学家拿起筷子的动作指定规则,避免饥饿和死锁发生

信号量设置

定义互斥信号量数组chopstick[5]={1,1,1,1,1},用于对5个筷子的互斥访问。哲学家按序编号0-4,哲学家i左边筷子的编号为i,右边筷子的编号为(i+1)%5

代码示例

semaphore chopstick[5]={1,1,1,1,1};
pi(){
    do{
        p(chopstick[i]);
        p(chopstick[(i+1)%5]);
        eat;
        v(chopstick[i]);
        v(chopstick[(i+1)%5]);
        think;
    }while(1);
}

这个算法存在一个问题,当5个哲学家都要进餐,并且都拿起了左边的筷子时,所有进程都无法获取右边的筷子,全部阻塞等待其他哲学家释放筷子,造成死锁。

为防止死锁产生,可以加一些限制条件:

①至多运行4名哲学家同时进餐,这个时候即使4个哲学家都拿起了左边的,但是第五根筷子可以被其中一个哲学家拿起,然后正常推进。

②仅当哲学家能够同时拿起左右两根筷子时,才允许他拿起筷子

③对哲学家顺序编号,奇数号先拿左边再拿右边,偶数号相反。

代码示例

semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex=1;		//保证每次只有一个哲学家能够访问筷子
pi(){
    do{
        p(mutex);		//如果已经有哲学家在访问筷子,则阻塞
        p(chopstick[i]);	//否则直接拿起两根筷子,顺利推进
        p(chopstick[(i+1)%5]);
        v(mutex);
        eat;
        v(chopstick[i]);
        v(chopstick[(i+1)%5]);
        think;
    }while(1);
}

不因为有筷子能拿起就拿起,而考虑能否一次性拿起两根筷子。这是哲学家进餐问题的关键。

4.吸烟者问题

问题描述

三个吸烟者,一个供应者。每个吸烟者需要的材料不同,共需要三种材料:烟草,纸,胶水。他们分别只有其中一种,剩下的两种需要供应者提供。供应者可以无限的提供三种材料,每次只随机提供其中的两种(意味着每次只有一个吸烟者的材料能够凑齐)。凑齐材料的吸烟者成功吸烟之后,供应者才能继续提供。

问题分析

关系分析

供应者和吸烟者属于同步关系,只有供应者供应了自己所需要的材料时,吸烟者才可以正常消费。三个吸烟者不能同时吸烟,属于互斥关系。

信号量设置

设置四个同步信号量,

offer1=0:第一个吸烟者需要的材料

offer2=0:第二个吸烟者需要的材料

offer3=0:第三个吸烟者需要的材料

finish=0:吸烟动作完成,供应者可以继续提供

代码示例

int random;
semaphore offer1=0;
semaphore offer2=0;
semaphore offer3=0;
semaphore finish=0;
//供应者
process p1(){
    while(1){ 
        random=任意一个整数随机数;
        random=random%3;
        switch(random){
            case 0:
                v(offer1);		//供应的材料不同,释放的资源也不一样
                break;
            case 1:
                v(offer2);
                break;
            case 2:
                v(offer3);
                break;
        }
        supply the material;
        p(finish);
    }
}
process p2(){
    p(offer1);			//只在自己需要的材料释放之后,才可以正常推进,否则阻塞
   	get the material and smoke;
    v(finish);			//在拿到材料之后,供应者才可以继续提供下一组材料
}
process p3(){
    p(offer2);
   	get the material and smoke;
    v(finish);
}
process p4(){
    p(offer3);
   	get the material and smoke;
    v(finish);
}
原文地址:https://www.cnblogs.com/ericling/p/13671562.html