在这个部分,我总是难以放下一个观点,就是觉得算法的设计故意设计的很蠢,下面会逐一说明。
首先看算法一:单标志法。
核心思想:设置一个公共整形变量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有没有可乘之机?因为这是对算法三的改良,成了讨论算法三是否会有两个同时进入的情况,算法三又是对算法二的改良。因为一个进程进来就宣布了自己想访问,所以对方检查的时候就知道了,所以,互斥也是必然的。
以上。