进程互斥的软件实现方法
单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予
int turn = 0; //turn 表示当前允许进入临界区的进程号P0 进程:
while (turn != 0); ① //进入区
critical section; ② //临界区
turn = 1; ③ //退出区
remainder section; ④ //剩余区P1进程:
while (turn != 1); ⑤ //进入区
critical section; ⑥ //临界区
turn = 0; ⑦ //退出区
remainder section; ⑧ //剩余区
turn 的初值为 0,即刚开始只允许 0 号进程进入临界区。 若 P1 先上处理机运行,则会一直卡在 ⑤。直到 P1 的时间片用完,发生调度,切换 P0 上处理机运行。代码 ① 不会卡住 P0,P0 可以正常访问临界区,在 P0 访问临界区期间即时切换回 P1,P1 依然会卡在 ⑤。只有 P0 在退出区将 turn 改为 1 后,P1 才能进入临界区。
因此,该算法可以实现“同一时刻最多只允许一个进程访问临界区”
存在的问题:
场景举例说明
让我们一步步模拟:
-
初始状态:
-
turn = 0
→ 表示现在轮到 P0 可以进入临界区 -
P1 想进入临界区,于是执行
while (turn != 1)
,发现不满足,只能等待 -
P0 其实此时没什么事,不想进临界区,也不运行相关代码
-
-
结果:
-
P1 一直卡在
while (turn != 1)
,根本进不去临界区; -
而
turn
又不会自动改变(P0 不进入临界区,就不会执行turn = 1
); -
P1 陷入“饥饿”状态:它想进入,但“钥匙”还在 P0 手上;
-
同时,P1 在那儿空等浪费 CPU(忙等待)→ 资源浪费
-
双标志先检查法
算法思想:
-
每个进程设置一个布尔型变量
flag[i]
来表示自己是否想进入临界区。 -
进程在进入临界区之前,先检查另一个进程的
flag
值,看对方是否也想进入。 -
如果对方也想进入,就进入忙等待。
-
如果对方没有想进入,则自己设置自己的
flag
为true
,然后进入临界区。
设置一个布尔型数组 flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0] = true”意味着 0 号进程 P0 现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区P0 进程:
while (flag[1]); ①
flag[0] = true; ② //进入区
critical section; ③
flag[0] = false; ④
remainder section;P1 进程:
while (flag[0]); ⑤ //如果此时 P0 想进入临界区,P1 就一直循环等待
flag[1] = true; ⑥ //标记为 P1 进程想要进入临界区 (进入区)
critical section; ⑦ //访问临界区
flag[1] = false; ⑧ //访问完临界区,修改标记为 P1 不想使用临界区
remainder section;
存在的问题
❗1. 不能保证互斥(互斥性失效)(按①⑤②⑥这样的顺序执行就会导致这样)
场景设定:
两个进程 P0 和 P1 几乎同时想进入临界区。
步骤拆解:
-
系统调度让 P0 和 P1 几乎同时执行到
while(flag[对方])
:-
P0 执行
while(flag[1])
,发现flag[1] == false
(因为 P1 还没设置) -
P1 执行
while(flag[0])
,也发现flag[0] == false
(因为 P0 还没设置)
-
-
因此:
-
P0 认为 P1 没想进,就放心地执行
flag[0] = true
-
P1 也认为 P0 没想进,就也执行
flag[1] = true
-
-
结果:P0 和 P1 都设置了自己的
flag
为true
,都进入了临界区! -
❌ 互斥性就失败了:两个进程同时访问共享资源。
若按照①⑤②⑥③⑦...的顺序执行,P0 和 P1 将会同时访问临界区。 因此,双标志先检查法的主要问题是:违反“忙则等待”原则。 原因在于,进入区的“检查”和“上锁”两个处理不是一气呵成的。“检查”后,“上锁”前可能发生进程切换。
双标志后检查法
算法思想:
先声明意图(上锁)再检查对方是否也要进。
这样做的原因是:
在先检查后上锁(比如前面的双标志先检查法)中,两个进程都可能误以为对方没想进,然后都进入临界区,违反了互斥原则。
所以这里改进成:
-
每个进程先把自己的
flag[i]
设置为true
,表示“我要进临界区”; -
然后检查对方的
flag[j]
; -
如果对方也想进,就等待;
-
如果对方不想进,就进入临界区。
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区P0 进程:
flag[0] = true; ①
while (flag[1]); ②
critical section; ③
flag[0] = false; ④
remainder section;P1 进程:
flag[1] = true; ⑤ //标记为 P1 进程想要进入临界区
while (flag[0]); ⑥ //如果 P0 也想进入临界区,则 P1 循环等待
critical section; ⑦ //访问临界区
flag[1] = false; ⑧ //访问完临界区,修改标记为 P1 不想使用临界区
remainder section;
若按照①⑤②⑥...的顺序执行,P0 和 P1 将都无法进入临界区 因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待”原则,会因各进程都长期无法访问临界资源而产生“饥饿”现象。
如果两个进程同时几乎执行到①⑤这一步:
-
P0
执行flag[0] = true;
-
P1
执行flag[1] = true;
-
然后:
-
P0
进入while (flag[1]);
,发现flag[1] == true
→ 开始等待 -
P1
进入while (flag[0]);
,发现flag[0] == true
→ 也开始等待
-
于是,两个进程都在等待对方放弃进入临界区,但又谁都不愿意主动放弃 → 形成了“互相谦让”却“都进不去”的尴尬状态
这就是所谓的:
❗ 饥饿现象(Starvation)和活锁(Live Lock)
-
饥饿(Starvation):进程长期得不到所需资源(临界区)
-
活锁(Live Lock):进程虽然一直在“活动”地等待(没有卡死),但实际上也永远得不到执行的机会
Peterson 算法代码
bool flag[2] = {false, false}; // 表示两个进程是否想进
int turn = 0; // 表示让谁先进入(初始给 P0)// P0 进程:
flag[0] = true; // ① 表示想进入临界区
turn = 1; // ② 表示让对方先试
while (flag[1] && turn == 1); // ③ 如果 P1 想进 且 turn 让 P1 先 → 等
critical section; // ④ 临界区
flag[0] = false; // ⑤ 退出临界区,表示不想进了// P1 进程:
flag[1] = true;
turn = 0;
while (flag[0] && turn == 0);
critical section;
flag[1] = false;