实现临界区互斥的基本方法
我们说临界资源是在一段时间内只允许一个进程访问,而进程访问临界资源的那段代码称为临界区
为了防止两个进程同时进入临界区访问临界资源,我们设计的同步机制就需要满足下面的准则
- 空闲让进:不能占着茅坑不拉屎,当临界区空闲时可以允许另一个进程请求并进入临界区
- 忙则等待:不能别人上着呢冲进去也要拉,当临界区被占用时,其他请求访问的进程必须等待
- 有限等待:不能让等待的人憋死了,防止进程饥饿,要保证进程能在有限的时间内进入临界区
- 让权等待:不能占着临界区、阻塞了还得占着CPU,防止忙等待
软件实现
一般这种方法都是基于全局变量的,让全局变量作为标志位
单标志法
最简单的一个想法其实就是设一个bool变量,0时表示P0进程可以进入,1时表示P1进程可以进入,假设全局bool变量为flag
// P0进程
while(flag != 0); // 当flag不为0时,说明不允许P0进入临界区,则循环等待 进入区
// 访问临界资源... 临界区
flag = 1; // 访问完了 要让给P1进程访问了 退出区
// 其他代码 剩余区
// P1进程
while(flag != 1); // 当flag不为1时,说明不允许P1进入临界区,则循环等待 进入区
// 访问临界资源... 临界区
flag = 0; // 访问完了 要让给P0进程访问了 退出区
// 其他代码 剩余区
这个算法是能够实现每次只让其中一个进程访问临界区的,但是每次访问完之后,必须让对方用,因为只有对方能把flag设置为自己的数字
这就相当于自己拉完了让别人拉,但是如果别人一直不拉,自己想拉也不能再拉,因为对方没有先拉,只有对方拉完了之后自己才能被允许
这就违背了空闲让进的原则,容易造成资源利用不充分,而且也会造成进程饥饿
双标志先检查法
有了之前的经验之后,我们要解决遗留下来的问题,那就再设一个标志位,bool flag[2]
flag[0]表示P0进程进入临界区的意愿,flag[1]表示P1进程进入临界区的意愿
假如P0进程想访问临界区,就先查看P1进程是否想用(flag[1]是否为true),是的话就等待,不是的话就把自己的设为true,然后访问临界区,访问完之后把自己设为false
这就相当于一个人在拉之前先问一句,你要不要拉,你要拉的话那你先,你不拉那我要拉了,拉完之后跟他说拉完了,你可以去了
// P0
while(flag[1]); // 检查P1是否在拉,在的话一直循环等待 进入区
flag[0] = true; // 表示自己要拉,其他人要拉的话必须等待 进入区
// 访问临界区资源... 临界区
flag[0] = false; // 表示自己拉完了,对方可以去 退出区
// 其他代码 剩余区
// P1
while(flag[0]); // 检查P0是否在拉,在的话一直循环等待 进入区
flag[1] = true; // 表示自己要拉,其他人要拉的话必须等待 进入区
// 访问临界区资源... 临界区
flag[1] = false; // 表示自己拉完了,对方可以去 退出区
// 其他代码 剩余区
这个算法就完美解决了必须交替执行的问题,如果对方一直不想用的话自己就可以一直用了
但是别忘了进程是并发执行的,而且具有异步性,这就导致一个巨大的问题
一开始while循环的时候,两个标志位都是false,都没人要用,然后又几乎同时说自己要用就开始拉了,同时把标志位设置为true了
着就违反了忙则等待的原则,主要原因其实就是检查和设置的操作这个过程有可能会被中断并发换下来
双标志后检查法
那么把进入区的两行代码换一下呢
// P0
flag[0] = true; // 表示自己要拉,其他人要拉的话必须等待 进入区
while(flag[1]); // 检查P1是否在拉,在的话一直循环等待 进入区
// 访问临界区资源... 临界区
flag[0] = false; // 表示自己拉完了,对方可以去 退出区
// 其他代码 剩余区
// P1
flag[1] = true; // 表示自己要拉,其他人要拉的话必须等待 进入区
while(flag[0]); // 检查P0是否在拉,在的话一直循环等待 进入区
// 访问临界区资源... 临界区
flag[1] = false; // 表示自己拉完了,对方可以去 退出区
// 其他代码 剩余区
这就相当于先声明我要拉,后一步检查对方要不要,但是我们继续考虑并发的情况,如果两个进程几乎同时声明自己要拉,检查的时候两个进程都会一直循环等待,结果就导致谁都没办法拉了
这就违背了空闲让进的原则了,也就会导致饥饿现象
似乎进入了死局?
Peterson算法
既然两个标志不够完成我们的要求,三个标志足够吗
Peterson算法就是这么干的,利用flag[2]表示想不想拉,用turn表示有没有进去拉,这样就把有限等待和空闲让进都解决了
// P0
flag[0] = true; // 表示自己想拉
turn = 1; // 表示对方可以拉,谦让一下
while(flag[1]&&turn==1); // 如果对方谦让了,并且对方没有想拉,那自己就拉 进入区
// 访问临界资源... 临界区
flag[0] = false; // 表示自己拉完了 退出区
// 其他代码 剩余区
// P1
flag[1] = true; // 表示自己想拉
turn = 0; // 表示对方可以拉,谦让一下
while(flag[0]&&turn==0); // 如果对方谦让了,并且对方没有想拉,那自己就拉 进入区
// 访问临界资源... 临界区
flag[1] = false; // 表示自己拉完了 退出区
// 其他代码 剩余区
即便两个进程并发执行,turn也只有一个值,也就是两个循环必定会走一个,就完美的遵循了前三个原则,但还没有完成让权等待的功能