软件实现临界区互斥的方法总结思考(四种算法的递进推导)

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

首先看算法一:单标志法

核心思想:设置一个公共整形变量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
}

仔细思考,可以明白,重点是在于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了
//剩余区

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

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

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

P0进程:
t0 = 1;
while(t1) ; //空等
//进入临界区;
t0 = 0; // 退出来,告诉对方自己OK了
//剩余区

P1进程:
t1 = 1;
while(t0) ; //空等
//进入临界区;
t1 = 0; // 退出来,告诉对方自己OK了
//剩余区

算法四: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;
剩余区;

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

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

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

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

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

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

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

以上。

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