进程同步二

信号量及PV操作

信号量机制是一种功能较强的机制,可用来解决互斥与同步问题,它只能被两个标准的原语wait(S)和signal(S)来访问,也可以记为”P操作”和”V操作”

如何理解信号量及PV操作(我印象中iOS的互斥锁就是基于PV操作实现的,在面试中也会经常考察到这部分)

首先我们假定有一个只有三个停车位的停车场(停车位代表临界资源,车代表访问资源的代码),因为有时候会出现停车场停满的现象,这个时候来的车就要在一个队列里面等着。

我们来模拟一下有五辆车先后进入,之后又按进入时间依次离开(非必须)的情况。

最开始的时候S=3,代表有三个空的停车位。

1号车进入,S=2;

2号车进入,S=1;

3号车进入,S=0;

4号车进入,P,S=-1(代表有1辆车在等待);

5号车进入,P,S=-2;

1号车离开,V,S=-1,4号车进入;

2号车离开,V,S=0,5号车进入;

3号车离开,V,S=1;

4号车离开,V,S=2;

5号车离开,V,S=3;

用什么数据结构实现PV操作呢?分两种信号量

1.整型信号量

wait(S){

while(S<=0);

S = S-1;

}

signal(S){

S = S+1;

}

在wait里面,只要信号量S<=0,就会不断的循环,违背让权等待规则。

2.记录型信号量

用一个value代表资源数目,增加一个进程链表(我觉得队列也OK),用于链接所有正在等待的进程。当执行P操作后,自我阻塞,将正在等待的进程放入进程链表中;当执行V操作后,取出链表中的一个元素,将其唤醒,放入临界区。

typedef struct{

int value;

struct process *L;

} semaphore

void wait(semaphore S){

S.value--;

if(S.value < 0){

add this process to S.L;

block(S.L);

}

}

void signal(semaphore S){

S.value++;

if(S.value<=0){

remove a process P from S.L;

wakeup(P);

}

}

用信号量实现同步的互斥的基本模型

同步(公共信号量初始化为0)

semaphore S=0;

P1(){ P2(){

... ...

x; P(S); //准备执行y了

V(S); //通知P2,x已完成 y;

... ...

} }

互斥(公共信号量初始化为1)

semaphore S=1;

P1(){                         P2(){

...                              ...

P(S);                         V(S);

进程P1的临界区;     进程P2的临界区;

V(S);                         P(S);

...                             ...

}                              }

用信号量可以实现前驱关系,实现方法如下

假设一个有向图G,满足G=(V,E),V={S1,S2,S3,S4,S5,S6},E={,,,,,,}

实现算法如下

semaphore a1=a2=b1=b2=c=d=e=0;

//在这里,我让=a1,=a2,=b1,=b2,=d,=e,=c

S1(){

...;

V(a1);

V(a2);//表示S1已经运行完成

}

S2(){

P(a1);

...;

V(b1);

V(b2);

}

以此类推,就不赘述了。

接下来重点将一个几个经典的同步问题(考研重点,面试从来没人问过)

1.生产者消费者问题

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

解答思路:首先根据最后一句话,缓冲区是临界资源,我们可以得出需要互斥的进去这个缓冲区。所以我们设置一个互斥变量(mutex=1),每当需要进入缓冲区的时候记得互斥一下。

之后我们根据题目要求,生产者生产的前提条件是缓冲区不满,也就是有闲的缓冲区(empty=n);消费者消费的前提条件是缓冲区不空,也就是有满的缓冲区(full=0)。

完整代码

semaphor mutex=1;

semaphore empty=n;

semaphore full=0;

pruduct(){

while(1){

生产...;

P(empty);//也就是要使用一个空的缓冲区

P(mutex);//互斥

把生产出来的东西放进缓冲区;

V(mutex);//互斥

V(full);//空的缓冲区少了一个,取而代之的是生产了一个满缓冲区

}

}

consumer(){

while(1){

P(full);//也就是要使用一个满的缓冲区

P(mutex);//互斥

消费掉生产出来的东西;

V(mutex);//互斥

V(empty);//满的缓冲区少了一个,取而代之的是生产了一个空的缓冲区

}

}

2、吃水果问题

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

解答思路:因为在这里,所放的水果有两种,而且只能放一个水果,所以和之前的生产者消费者问题有了很大的不同。首先我们放水果吃水果都应该是一个互斥的过程,所以设置一个用于表示只有一个盘子的变量(plate=1)。

之后每次爸爸生产出来的是苹果,女儿消费的也是苹果,所以设置一个表示苹果的变量(apple=0)。同理,设置表示橘子的变量(orange=0)。

完整代码

semaphore plate =1,apple=0,orange=0;

dad(){

while(1){

准备苹果;

P(plate);//互斥的向盘中取、放水果

向盘子里放苹果;

V(apple);//盘子中的苹果++,也可以理解为叫女儿过来吃苹果

}

}

daughter(){

while(1){

P(apple);//收到了爸爸的放苹果信息,准备来吃了,苹果--

拿走苹果;

V(plate);//拿走了苹果,盘子中的东西--,也可以理解为叫爸爸妈妈再放

}

}

妈妈和儿子同理,这里就不赘述了。

在这里我提出一个问题,如果盘子里可以放两个水果,拿代码需要做怎样的修改?

我的想法是,单纯的把plant的数量修改为2就可以,但是这样的话,放水果和吃水果就不是一个互斥的行为了,如果题目没要求互斥,就没问题了,如果题目要求必须是互斥的,则需要再加一个额外的互斥变量,保证放和吃的互斥(同理,放不互斥或吃不互斥,只要单独修改就好了,加上互斥变量就是互斥,不加就不互斥)

3、读者写者问题

问题描述:有读者和写者两组并发进程,共享一个文件,当两个和两个以上的读进程同时访问数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问就会导致数据的不一致。

所以我们要求,1.允许多个读者同时访问文件2.只允许一个写者往文件中写信息3.任一写者完成操作前不允许其他读者或写者工作4.写者在执行写操做前,应确保没有其他读者写者正在访问文件。

解答思路:首先读者的数量count,它的增加或者减少都不能让任何人唤醒或等待,所以count不是信号量,count应该是一个计数器。

读者和写者要互斥访问,所以我们要设置一个变量(rw=1)。

如果这样子就结束了,我们让读者执行:判断读者数量(若为0则P(rw)) -> count++ -> 读-> count-- ->判断读者数量(若count为0则V(rw))

就会产生一个问题,假如两个读者同时达到,此时count=1,这样两个人同时执行count++,之后count有可能会=2,但是实际上这个时候已经有三个读者进去了。

所以我们在进行count++,count--操作时,应该加一个互斥锁(mutex=1),这样就可以避免这种情况了。

完整代码

int count=0;

semaphore mutex=1;

semaphore rw=1;

writer(){

while(1){

P(rw);

writing;

V(rw);

}

}

reader(){

while(1){

P(mutex);//互斥访问count变量

if(count==0)//如果count==0,也就是第一个进来的读者

P(rw);//如果有写者在里面,就会等待,没有,就会让以后写者进不来

count++;

V(mutex);


读吧...;


P(mutex);//互斥访问count

count--

if(count==0)//如果这个是最后一个读者,这样的话把外面等着的写者叫起来吧

V(rw);

V(mutex);

}

}

这样子的话是一个读者优先,有时候有其他变形。

我们按照到达顺序和执行顺序可以把执行方式分为三类

到达顺序:RRWRRWWWRRR

执行顺序1:RRRRRRRWWWW (读者优先)

执行顺序2:RRWRRWWWRRR (顺序优先)

执行顺序3:RRWWWWRRRRR (写者优先)

顺序优先

解答思路:顺序优先要给双方加限制,当写者到的时候,P一下,后面到的读者都要等待。当读者到的时候,P一下(其实主要是为了确保前面没有写者),如果有写者就等待,如果没写者就进去(先执行修改count拿一系列操作),记得要在读之前V一下,保证下一个读者能顺利进入。

简单可以理解为,双方进之前都要进行P操作,但是写者只有在写完了才V,读者要在读之前V。

完整代码

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);

P(mutex);//互斥访问count变量

if(count==0)//如果count==0,也就是第一个进来的读者

P(rw);//如果有写者在里面,就会等待,没有,就会让以后写者进不来

count++;

V(mutex);

V(w);


读吧...;


P(mutex);//互斥访问count

count--

if(count==0)//如果这个是最后一个读者,这样的话把外面等着的写者叫起来吧

V(rw);

V(mutex);

}

}

写者优先

实现方法较为复杂,先放上代码

写者优先只要保证WRW形式的进入方式是WWR就可以了,也就是当第一个W进入的时候,把wfirst按住,这样R就进不来。接下来下一个W就会跳过if语句。但是因为不可以两个写者同时写,所以停在wmutex外面,当W读完之后,直接放下一个W进来,直到所有W都写完了,才会放R进来。

完整代码

semaphoRe rmutex=1,mutex=1,wfirst=1,writemutex=1;

int readcount=0.writecount=0;

reader(){

while(1){

P(wfirst);

if(readcount==0)

P(wmutex);

readcount++;

V(rmutex);

读吧...;

V(wfirst);

P(rmutex);

readcount--;

if(readcount==0)

V(wmutex);

V(rmutex);

}

}

writer(){

while(1){

P(writemutex)

if(writecount==0)

P(wfirst);

writecount++;

V(writemutex);

P(wmutex);

写吧...;

V(wmutex);

P(writemutex);

if(writecount==0)

V(wfirst);

writecount--;

V(writemutex);

}

}

4、哲学家进餐问题

问题描述:一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆着一根筷子,桌子的中间是一碗米饭。

哲学家只进行吃饭和思考,当哲学家饥饿的时候,拿起左右筷子(一根一根的拿起)。如果筷子在别人手上,则需要等待。只有同时拿到了两根筷子哲学家才可以进餐。

解答思路

定义互斥信号量数组chopstick[5]={1,1,1,1,1}

哲学家按顺序从0~4编号,哲学家i左边筷子编号为i,右边筷子编号为(i+1)%5

第一种做法

把取左筷子和取右筷子作为一个原子操作,加上锁进行,就可以了(放回去就不用加锁了)。

具体代码

Pi(){

while(1){

P(mutex);

P(chopstick[i]);

P(chopstick[(i+1)%5]);

V(mutex);

eat;

V(chopstick[i]);

V(chopstick[(i+1)%5]);

think;

}

}

AMD信号量机制可以非常简洁的写完代码

Pi(){

while(1){

Swait(chopstick[(i+1)mod 5),chopstick[i]);

eat;

Signal(chopstick[(i+1)mod 5),chopstick[i]);

}

}

第二种做法

用奇偶数的方法,让奇数哲学家先拿它左边的筷子,然后在去拿右边的筷子,偶数哲学家相反。

这样,1、2号哲学家竞争1号筷子;3、4号哲学家竞争3号筷子,最后无论如何都能有哲学家吃到饭。

具体代码

Pi(){

If(i%2==0){//偶数哲学家,先右后左

P(chopstick[i+1]%5);

P(chopstick[i]);

Eating();

V(chopstick[i+1]%5);

V(chopstick[i]);

}

else{//奇数哲学家,先左后右

P(chopstick[i]);

P(chopstick[i+1]%5);

Eating();

V(chopstick[i]);

V(chopstick[i+1]%5);

}

}

第三种做法

至多只允许4位哲学家同时去取左边的筷子,最终能保证至少有一位哲学家能够进餐

具体代码

semaphore eating=4;

Pi(){

while(1){

P(eating);

P(chopstick[i]);

P(chopstick[(i+1)%5]);

eating();

V(chopstick[i]);

V(chopstick[(i+1)%5]);

V(eating);

}

}

5、吸烟者问题

问题描述:假设一个系统有三个抽烟者和一个供应者。每个抽烟者不停地卷烟并抽掉,抽烟者需要三种材料:烟草、纸、胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸、第三个拥有胶水。供应者无限的供应三种材料,供应者每次将两种材料放到桌子上,拥有剩下的那两种材料的男人卷一根烟抽到,并给供应者一个信号告诉完成了,供应者就会再放两种材料在桌子上。

直接上代码吧

Int random;//存储随机数

Semaphore offer1=0;//烟草和纸

Semaphore offer2=0;//烟草和胶水

Semaphore offer3=0;//纸和胶水

Semaphore finish=0;//是否完成

P1(){

Random =任意一个整数;

Random = random%3;

If(random==0)

V(offer1);

If(random==1)

V(offer2);

If(random==2)

V(offer3);

P(finish);

}

}

P2(){

While(1){

P(offer3);

拿出纸和胶水,抽掉;

V(finish);

}

}

我觉得这个直接把题目改成,爸爸往盘子里放苹果、桃子、梨。老大只吃苹果,老二只吃桃子,老三只吃梨,每次盘子里只能放一个水果,吃完之后再让爸爸放。

这样的话这个题目叫好理解很多了。

你可能感兴趣的:(进程同步二)