解决临界区(互斥)的软件方法-Dekker算法和Peterson算法

来源:互联网 发布:淘宝设置优惠券要钱吗 编辑:程序博客网 时间:2024/06/05 19:29

这里写图片描述
Dekker算法初步设想【保证了互斥】:让两个进程共享一个全局变量turn,【其初值为0或1】。如果turn==i,那么进程Pi允许在其临界区内执行。
—为了控制两个进程互斥进入临界区,可以让两个进程轮流进入临界区。
—当一个进程正在临界区执行时,另一个进程就不能进入临界区,而在临界区外等待。

这里写图片描述

这里写图片描述
1.为何是“强制交替”?
Turn=0时,无论CS空闲与否,即使P1就绪想要进入CS也【不会成功】,必须等待P0进入CS执行,退出后才能进入CS。
【关键】turn=0时P1:while(turn!=1);会一直陷入循环【忙等】,直至P0运行一次,turn值改为1,P1进程才可以运行。(一般turn=0是P1执行的结果,即P1执行后P1不能再次执行,必须换到P0执行。所以是“强制交替”)
2.为何不满足“前进要求”,任何进程在CS 区内外失败,其他进程都可能因为等待使用CS而无法向前推进?
可能恰恰是因为“强制交替”,假若P0和P1两个进程交替,P0进程如果没有修改turn=1就死了,那么P1就永远无法运行。

Dekker算法-改进一:
【初步设想】让两个进程共享一个全局变量turn,【其初值为0或1】。如果turn==i,那么进程Pi允许在其临界区内执行。
【改进一】使用全局共享数组flag标志CS状态:
flag[0]或flag[1]=true:表示P0或P1占用CS,
flag[0]或flag[1]=false:表示CS空闲。
这里写图片描述
这里写图片描述
1.进程在CS内失败?什么意思?
【未解决】2017.11.29
2.不能实现互斥,P0和P1可能同时进入临界区。【初步设想是实现了互斥的】

这里写图片描述
Dekker算法-改进二【改为先置标志位】:
这里写图片描述
这里写图片描述
这里写图片描述
Dekker算法-【最终成功版】:
这里写图片描述

flag[2]用来表示是否想要使用关键区,turn用来表示具有访问权限的进程ID。(重点看注释,通过注释,挺好理解的哟~)

#include<stdio.h>  #include<stdlib.h>  #include<pthread.h>  #define true 1  #define false 0  typedef int bool;  bool flag[2];  int turn;  void visit(int num)  {          sleep(1);          printf("P%d is visting\n",num);  }  void P0()  {          while(true)          {                  flag[0] = true;//P0想使用关键区。                  while(flag[1])//检查P1是不是也想用?                  {                          if(turn == 1)//如果P1想用,则查看P1是否具有访问权限?                          {                                  flag[0] = false;//如果有,则P0放弃。                                  while(turn == 1);//检查turn是否属于P1。                                  flag[0] = true;//P0想使用。                          }                  }                  visit(0); //访问Critical Partition。                  turn = 1; //访问完成,将权限给P1。                  flag[0] = false;//P0结束使用。          }  }  void P1()  {          while(true)          {                  flag[1] = true; //P1想使用关键区。                  while(flag[0]) //检查P0是不是也想用?                  {                          if(turn == 0) //如果P0想用,则查看P0是否具有访问权限?                          {                                  flag[1] = false; //如果有,则P1放弃。                                  while(turn == 0); //检查turn是否属于P1。                                  flag[1] = true; // P1想使用。                          }                  }                    visit(1); //访问Critical Partition。                  turn = 0; //访问完成,将权限给P0。                  flag[1] = false; //P1结束使用。          }  }  void main()  {          pthread_t t1,t2;          flag[0] = flag[1] = false;          turn = 0;          int err;          err =  pthread_create(&t1,NULL,(void*)P0,NULL);          if(err != 0) exit(-1);          err = pthread_create(&t2,NULL,(void*)P1,NULL);          if(err != 0 ) exit(-1);          pthread_join(t1,NULL);          pthread_join(t2,NULL);          exit(0);  }  

Dekker算法
Dekker互斥算法是由荷兰数学家Dekker提出的一种解决并发进程互斥与同步的软件实现方法。
参数说明:
两个全局共享的状态变量flag[0]和flag[1],表示临界区状态及哪个进程想要占用临界区,初始值为0。
全局共享变量turn(值为1或0)表示能进入临界区的进程序号,初始值任意,一般为0。
算法原理:
设有进程P0和P1,两者谁要访问临界区,就让对应的flag=true(例如P0要访问临界区,就让flag[0]=true),相当于“举手示意我要访问”。初始值为0表示一开始没人要访问。
trun用于标识当前允许谁进入,turn=0则P0可进入,turn=1则P1可进入。

1)P0的逻辑do{    flag[0] = true;// 首先P0举手示意我要访问    while(flag[1]) {// 看看P1是否也举手了        if(turn==1){// 如果P1也举手了,那么就看看到底轮到谁            flag[0]=false;// 如果确实轮到P1,那么P0先把手放下(让P1先)            while(turn==1);// 只要还是P1的时间,P0就不举手,一直等            flag[0]=true;// 等到P1用完了(轮到P0了),P0再举手        }    }}visit();// 访问临界区turn = 1;// P0访问完了,把轮次交给P1,让P1可以访问flag[0]=false;// P0放下手2)P1的逻辑do{    flag[1] = true;// 先P1举手示意我要访问    while(flag[0]) {// 如果P0是否也举手了        if(turn==0){// 如果P0也举手了,那么久看看到底轮到谁            flag[1]=false;// 如果确实轮到P0,那么P1先把手放下(让P0先)            while(turn==0);// 只要还是P0的时间,P1就不举手,一直等            flag[0]=true;// 等到P0用完了(轮到P1了),P1再举手        }    }}visit();// 访问临界区turn = 0;// P1访问完了,把轮次交给P0,让P0可以访问flag[1]=false;// P1放下手

Peterson算法
Peterson算法也是保证两个进程(线程)实现互斥的方法,比之前的Dekker算法更加简单,他同样提供了两个变量,保证进程不进入关键区,一个是flag[2],一个是turn,两者的表达意思也类似,flag数组表示能否有权限使用关键区,turn是指有访问权限的进线程ID。(注释很重要,帮助你理解)
这里写图片描述
这里写图片描述
这里写图片描述

#include<stdio.h>  #include<stdlib.h>  #include<pthread.h>  #define true 1  #define false 0  typedef int bool;  bool flag[2];  int turn;  void procedure0()  {          while(true)          {                  flag[0] = true;                  turn = 1;                  while(flag[1] && turn == 1)//退出while循环的条件就是,要么另一个线程                  //不想要使用关键区,要么此线程拥有访问权限。                  {                          sleep(1);                          printf("procedure0 is waiting!\n");                  }                  //critical section                  flag[0] = false;          }  }  void procedure1()  {          while(true)          {                  flag[1] = true;                  turn = 0;                  while(flag[0] && turn == 0)                  {                          sleep(1);                          printf("procedure1 is waiting!\n");                  }                  //critical section                  flag[1] = false;          }  }  void main()  {          pthread_t t1,t2;          flag[0] = flag[1] = false;          int err;          turn = 0;          err =  pthread_create(&t1,NULL,(void*)procedure0,NULL);          if(err != 0) exit(-1);          err = pthread_create(&t2,NULL,(void*)procedure1,NULL);          if(err != 0 ) exit(-1);          pthread_join(t1,NULL);          pthread_join(t2,NULL);          exit(0);  }  

Peterson算法是一个实现互斥锁的并发程序设计算法,核心就是三个标志位是怎样控制两个方法对临界区的访问,这个算法设计的想当精妙,我刚开始看的时候就被绕了一下。
算法使用两个控制变量flag与turn.
其中flag的值为真,表示ID号为n的进程希望进入该临界区.
标量turn保存有权访问共享资源的进程的ID号.

当时不明白这个算法关键是其中的一句话很迷惑不知道怎么理解
while(flag[1] && turn == 1)
为什么进程P1想要进去并且也轮到它了它会在一直死循环等待呢?
等价while(flag[1]==true && turn == 1)
还有flag[0] = false; ,为啥在1死循环之后,进程0又给设置不想进入临界区了?
经过多方查找资料之后,终于明白了,关键在于这个Peterson算法是两个进程啊!所以也要有两个这样的循环,接下来是重点:
注意到如果进程P0和P1并发,那么两者中必然会有一个会被while堵塞住(因为flag[0和1]均等于true),而另一个会完成自己的任务并置对方的flag位为false,这时while的条件不再满足,即可执行自己的程序,实现了互斥。
这两个进程是互相影响的,互相给对方设置flag,也就是说,程序应该是这样的:

P0: flag[0] = true; //P0举手表示想进入                       P1:flag[1] = true;    turn = 1; //表示轮到P1,P1具有访问权限                       turn = 0;    while (flag[1] == true && turn == 1)                      while (flag[0] == true && turn == 0)        {                                                     {        // busy wait                                               //busy wait        } //P1想进并且能进但是会陷入死循环等待                      }    // critical section   //P0进入临界区                        // critical section    flag[0] = false;                                          flag[1] = false;    // end of critical section                                // end of critical section

其中的关键是下面这句话
flag[0] = false; //P0执行完之后会设置flag值为false目的是让P1进程中的P0可以跳出死循环进而进入临界区执行任务
以此类推
flag[1] = false; //P1执行完之后会设置flag值为false目的是让P0进程中的P1可以跳出死循环而进入临界区执行任务
恩,就是这样的,在P0进程中,如果P0想进入临界区,那么会优先让P0进入临界区而P1死循环等待,直到P1的进程中P1执行完任务并把flag值设置为false之后,P0进程中的P1才能被释放进入临界区从而执行任务。它们是这样互相影响的……

在这个部分,我总是难以放下一个观点,就是觉得算法的设计故意设计的很蠢,下面会逐一说明。

首先看算法一:单标志法

核心思想:设置一个公共整形变量turn,用于指示被允许进入临界区的进程编号。若turn = 0, 表示允许P0进入临界区。

OK,到这里肯定很容易想到一个问题,谁来改变turn?这里的turn像是一把锁,控制着进程的进入。
如果是公共区域控制turn这个变量,比如turn = 0时,允许P0进入。那么P0不想进的时候,即使是turn = 0,对于进程P0也是没有价值的。公共区域无法预测谁想要,所以这个控制权还是分权给进程来管理比较好一些。
即:Pi想进入时检测turn值是否是自己的turn,如果不是,自然就要等。先不管turn怎么变到Pi可用的,这个得等到Pi进入后再可以说明。OK,终于等到了自己的turn,于是进入临界区执行,执行完退出,那么,不能就把锁这样仍然设置为自己可进,这样太mean了。所以可以更改turn为其他进程可进。如果是大于两道进程,这个设置就不知道怎么办了,所以通常情况下,都是考虑两道进程。所以好办了,改成P1就可以了(即只有P0,P1两道进程)。同样的,P1也这么认为,每当自己用完了,就考虑对方的感受。

但是,问题来了,如果P0用完,好心把turn变成了P1可用,然后P0还想回来再用,好了,得问P1答应不答应!

所以,这种设计思路,必定绝对了P0和P1只能交替使用。在某些场景中,这种思路是可行的。

看一眼算法描述:

P0:while(turn != 0){     // critical section     turn  = 1; // 退出区     // remainder section}P1:while(turn != 1){     // critical section     turn  = 0; // 退出区     // remainder section}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

仔细思考,可以明白,重点是在于turn的管理思路上的不同。

如果我们只用一个变量表示临界区是否可用,设为mutex,是0的时候表示有人在用了,那么只好等待。是1的时候,表示可用,进去就把mutex改为0,让别的进程进不来。这才更像是生活中的锁。

当然,这种思路就是最常见的PV操作,也是非常优秀的临界区控制算法思路。

两种思路的差别很小,但是反应的思想却天差地别。

算法二:双标志法之先检查

核心思想:每一个进程在访问临界区之前,先查看一下临界区是否正在被访问,如果正在被访问,等待,否则进入临界区。

这个核心思想听着和算法一压根没区别!

其实是,这个核心思想,根本就是临界区的基本要求。只不过是实现的思路有好有差罢了。

简单说来,双标志法就是,设置了t0, t1两个变量,也可以考虑用数组来表示,并把数组命名为flag[2],这只是细节的差别,不影响这个思路。

我们用两个变量,可以忽略掉很多人对为什么设计数组的下意识的疑问。
这个算法就是在说,每次自己想访问临界区的时候,先看对方是否已经在访问,比如P0想访问临界区,就看t1的值是否为0,如果为1,表示,P1正在开心的运行呢,不能打扰,让P1老老实实完成,等P1结束了,t1也会变为0,因此,P0得以进入,进入后,当然马上把t0设为1,表示自己进来了,不可打扰。但是,在P0检查完t1为0,自己有机会进来的时候,P0准备好进入,在它还未把锁锁上的时候,t1又要执行临界区,它一检查,好嘛,t0也等于0,不用等。进去一看,天哪,门后面有个P0正在锁门!完了,这下怎么办,两个人同时要访问临界区了!

所以,这个算法的漏洞在于:检查对方的turn和锁紧自己的turn之间存在着可乘之机
因此,只要能让检查和设置成为一个不可broken的事务,问题就不是问题了。
但是呢,你看,双标志法还是没有我们前面提到的PV来的更好。

同样的,P1想进入也是一个道理。

这里需要着重强调的是,不用交替进入了,因为,Pi出来后,就宣布,自己已经不占有临界区了,谁想来,自己来就好,不像单标志法中,出来还要想着把权力交给谁。交出去了,自己的命运就只能等着被对方决定。

所以这个算法实现了初步的解耦合

算法一览:

P0进程:while(t1) ; //空等t0 = 1;//进入临界区;t0 = 0; // 退出来,告诉对方自己OK了//剩余区P1进程:while(t0) ; //空等t1 = 1;//进入临界区;t1 = 0; // 退出来,告诉对方自己OK了//剩余区
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

算法三:双标志法之后检查

核心思想也是一样的,只不过我们将调整一下检查的顺序,变成当Pi想进去的时候,就大方的向世界宣布,我要进临界区!但是事实上还得费一番功夫才有机会,就像追一个人,你向外界宣布了你的决定,但并不代表你马上就能拥有。
然后还是要看对方是否在占用。等对方占用完毕,出来把自己设为0,于是等待的进程可以进去了。

这里的问题需要仔细思索一下:当P1刚刚结束访问的时候,P0检查到了机会,准备进入,这自然不会引起问题。而如果,当临界区空着呢,P0和P1同时过来,两个人都宣布自己要临界区,然后两个人互相检查对方的状态,发现啊,谁也进不去了啊。便产生了饥饿现象。

P0进程:t0 = 1;while(t1) ; //空等//进入临界区;t0 = 0; // 退出来,告诉对方自己OK了//剩余区P1进程:t1 = 1;while(t0) ; //空等//进入临界区;t1 = 0; // 退出来,告诉对方自己OK了//剩余区
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

算法四:Peterson 算法 : In honor of Peterson,解决了算法三的饥饿现象

这个算法呢,综合了算法1和算法3得到的综合产品,实现思路,不手动模拟不能体会。

核心思路:每个进程上来就是先向世界宣布自己想访问临界区,但是,虽然这么想,它还是谦虚的认为,这一轮我不抢,让对方先来!即,把turn设置为对方的。

而能让Pi空等的条件是,对方真的在访问且是对方的turn.
只要两个条件任何一个不满足,Pi就大方的进来了。
当然,这都是主观的行为。

P0:t0 = 1; turn = 1; // 我想要,但是我先等你一轮while(t1 && turn == 1); // t1真的在用,且turn是P1的,等待临界区;t0 = 0;剩余区;P1:t1 = 1; turn = 0; // 我想要,但是我先等你一轮while(t0 && turn == 0); // t0真的在用,且turn是P0的,等待临界区;t1= 0;剩余区;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

仔细看,这个和算法三相比,就多了一个绅士的动作,加一个turn为对方的条件,并且修改了等待的条件。

因此,我们看,饥饿是如何解决的:
首先,两个人都宣布了自己要访问临界区,大家坦陈相待,很酷。又加上一条,承认对方的turn。

OK,很美好。那么判断的时候,需要满足两个条件自己才等待。

算法三中,饥饿发生的情况是:互相检查对方的状态时,发现对方都为1(t0,t1),现在也假设这么干,无疑,t0,t1可以都为1,但是turn是共用的。P0设置turn为1,让P1进入这轮,P1呢,也很绅士,让turn位0,让P0先行。

因此,总有一个while的将会结束,因此,不会再有饥饿。

所以,总体还是很绅士的。
解决饥饿的方式就是两者不会再一起等。

其他的,再思考一下,能不能真的做到互斥呢?
很明显,是可以的。当P0在访问的时候,P1有没有可乘之机?因为这是对算法三的改良,成了讨论算法三是否会有两个同时进入的情况,算法三又是对算法二的改良。因为一个进程进来就宣布了自己想访问,所以对方检查的时候就知道了,所以,互斥也是必然的。

以上。

阅读全文
0 0
原创粉丝点击