2.13 互斥
要求
- 空闲让进:若空闲,申请即进
- 忙则等待:只允许临界区存在一个进程,若忙碌,区外等待
- 有限等待:进程等待的时间是有限的,不会造成死锁、饥饿
- 让权等待:进程不能在临界区长时间阻塞等待某事件
- 以上类比生活中任何公共资源都可,如公用电话
2.13.1 互斥:软件方法
思路
- 在进入区设置标志来判断是否有进程在临界区
- 若临界区已有进程,则循环等待
- 进程离开临界区后在退出区修改标志
第一代:轮换使用临界区
每个进入临界区的进程的权限只能被另一个进程赋予
int turn=0;
//进程P0
do{
while(turn!=0);//进入区
P0代码;//退出区
turn=1;//退出区
}while(true)
//进程P1
do{
while(turn!=1);//进入区
P1代码;//临界区
turn=0;//退出区
}while(true)
严格轮换,实现了互斥访问
违反了空闲让进的原则:比如turn=1时,临界区空,但P1无法进入
第二代:设置临界区状态标志
进入之前看有没有其他进程要进入,没有则将自己的状态置为true
boolean flag[2]={false,false}//该全局变量标志临界区空闲与否
//进程P0
do{
while(flag[1]);//进入区
flag[0]=true;//进入区
P0代码;//临界区
flag[0]=false;//退出区
}while(true)
//进程P1
do{
while(flag[0]);//进入区
flag[1]=true;//进入区
P1代码;//临界区
flag[1]=false;//退出区
}while(true)
违反了忙则等待原则:假设CPU先将时间片分给P0,然后P0判断flag[1]=false跳出循环向下执行,这时CPU又将时间片分给 P1,P0尚未执行到置flag[0]=true;P1判断flag[0]=false跳出循环向下执行,两者都将进入临界区
注意:while(flag[1]);这一句表明:当flag[1]=true时停在这一句,flag[1]=false时跳出循环执行下一句,P0即进入临界区
第三代:表明使用临界区的状态
先亮明状态,再循环等待
boolean flag[2] = {false, false};//共享的全局变量
//进程P0
do {
flag[0] = true; //进入区
while (flag[1]) ;//进入区
进程P0的临界区代码; //临界区
flag[0] = false; //退出区
进程P0的其它代码 //剩余区
} while (true)
//进程P1
do {
flag[1] = true; //进入区
while (flag[0]) ;//进入区
进程P1的临界区代码; //临界区
flag[1] = false; //退出区
进程P1的其它代码 //剩余区
} while (true)
违反了空闲让进原则:CPU先将时间片分给P0,然后P0置flag[0]=true,这时CPU又将时间片分给 P1,P1置flag[1]=true;那么接下来两者都在while循环处等待对方清零,都无法进入临界区,形成死锁
第四代:表明使用临界区的态度+谦让
预先表明想进入临界区,但为了防止死锁,在进入临界区前会延迟一段时间
boolean flag[2] = {false, false};//共享的全局变量
//进程P0
do {
flag[0] = true;
while (flag[1]) {
flag[0] = false;
<随机延迟一小段时间>;//谦让
flag[0] = true;
}
进程P0的临界区代码; //临界区
flag[0] = false; //退出区
进程P0的其它代码 //剩余区
} while (true)
//进程P1
do {
flag[1] = true;
while (flag[0]) {
flag[1] = false;
<随机延迟一小段时间>;//谦让
flag[1] = true;
}
进程P1的临界区代码; //临界区
flag[1] = false; //退出区
进程P1的其它代码 //剩余区
} while (true)
可能会活锁,过分谦让,长时间僵持:CPU先将时间片分给P0,然后P0置flag[0]=true,这时CPU又将时间片分给 P1,P1置flag[1]=true;接下来CPU先将时间片分给P0,P0发现flag[1]=true;进入while循环,先将flag[0]清零,再挂起一段时间,重新置位flag[0],查看flag[1]是否仍为1,若是则继续清零等待,重复这个过程,监听flag[1]的变化;同理,P1可能会与P0默契地进行同样的活动,两者同时监听,同时挂起,形成活锁,可能随时间自动解除
Dekker互斥算法
由于前一种算法活锁的原因是只监听了对方的flag,这时添加一个只能由对方改变的信息turn即可
boolean flag[2] = {false, false}; //共享的全局变量
int turn = 1; //共享的全局变量
//进程P0
do {
flag[0] = true; //进入区
while (flag[1]) {
if (turn == 1) {
flag[0] = false;
while (turn == 1) ; //等待
flag[0] = true;
}
} //进入区
进程P0的临界区代码; //临界区
turn = 1;
flag[0] = false; //退出区
进程P0的其它代码 //剩余区
} while (true)
//进程P1
do {
flag[1] = true; //进入区
while (flag[0]) {
if (turn == 0) {
flag[1] = false;
while (turn == 0) ; //等待
flag[1] = true;
}
} //进入区
进程P1的临界区代码; //临界区
turn = 0;
flag[1] = false; //退出区
进程P1的其它代码 //剩余区
} while (true)
在预先表明态度+谦让的基础上改进了谦让机制:
P1在退出临界区时会置turn=0;P0退出临界区会将turn置为1
这样一来,可以在监听对方flag时知道对方是否已经退出临界区,而不是等待一段随机时间,我认为这时重新尝试将自己的flag置为true是更好的做法,不会产生活锁
Peterson互斥算法
与dekker算法的区别:不是在退出时而是在进入前将turn改变
boolean flag[2] = {false, false}; //共享的全局变量
int turn; //共享的全局变量
//进程P0
do {
flag[0] = true; //进入区
turn = 1; //进入区
while (flag[1] && turn == 1) ; //进入区
进程P0的临界区代码; //临界区
flag[0] = false; //退出区
进程P0的其它代码 //剩余区
} while (true)
进程P1
do {
flag[1] = true; //进入区
turn = 0; //进入区
while (flag[0] && turn == 0) ; //进入区
进程P1的临界区代码; //临界区
flag[1] = false; //退出区
进程P1的其它代码 //剩余区
} while (true)
假设P0进程先置自己的flag为true,改变turn=1,此时有一个while循环,当P1的flag=false时,说明P1还没有运行到置flag=true的行,可以继续进入临界区,或当turn=0时,这时说明P1执行后转而执行P0,P0此时在turn行,但P0无法进入临界区,因为此时flag[0]=true,而此时P0可以无视flag[1]=true的条件而继续进入临界区
评价软件互斥方法
- 不能解决忙等问题(使用while监听方法),效率低
- 进程互斥使用临界资源困难,难以控制两个以上进程的互斥
- 算法设计时容易死锁或互斥失败