單標志法
算法思想:兩個進程在訪問完臨界區后會把使用臨界區的權限轉交給另一個進程。也就是說每個進程進入臨界區的權限只能被另一個進程賦予
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;