競爭條件
在進入本篇的主題之前,要先知道一些基本知識。
背景
假設今天有兩個進程 P1,P2,他們共享一個變量 COUNT,而 COUNT 初始值 = 0。
P1 和 P2 做的事情其實是一樣的,偽代碼如下:
.GLOBAL {
COUNT := 0
}
P1 {
R1 := COUNT
R1 := R1 + 1
COUNT := R1
}
P2 {
R2 := COUNT
R2 := R2 + 1
COUNT := R2
}
我們都知道進程之間可以順序執行也可以併發執行,那麼就來看看,如果 P1,P2 順序執行和併發執行會有甚麼區別。
順序執行
如上圖所示,如果今天順序執行(優先級默認 P1 > P2),那麼首先 R1 會變成 0,然後 ++ 變成 1,這時再寫回共享的 COUNT = 1。至此 P1 執行結束。
接著 P2 開始執行。首先 R2 會變成 1,然後 ++ 變成 2,這時再寫回共享的 COUNT = 2。至此 P2 執行結束。
最終 COUNT = 2。
併發執行
如上圖所示,如果今天是併發執行,我們假設執行一條 P1 語句 P2 就也要執行一個語句。那麼首先,從 P1 開始,R1 變成 0,這時 P2 也開始執行,R2 也變成 0。接著,R1++ 變成 1,R2++ 也變成 1,然後 COUNT 被賦值為 1(P1 最後一句),然後COUNT 又再一次被賦值為 1(P2 最後一句)。
看到這邊,相信大家也看出奇怪之處了,首先在併發執行的情況下,COUNT 被兩次賦值為 1 本身就很奇怪,再者,兩個進程都對共享變量做過 ++ 的操作,理論上 COUNT 應該要是 2 才對,但莫名的為 1。
總結一下,像這種多個進程併發訪問和操作共享資源,而執行結果與執行順序相關的情況就稱為競爭條件。
臨界區問題
臨界區英文叫做 critical section,就是為了解決上述競爭條件的問題。假設一個系統有 n 個進程,而每個進程都有一段代碼稱為臨界區。而存在臨界區的目的就在於,我們希望,當一個進程在自己的臨界區內執行時,其他進程就都不可以在自己的臨界區內執行。
臨界區必須滿足以下三個條件:
- 互斥 (Mutual exclusion)
互斥原則要求不能同時有兩個以上的進程在各自的臨界區內執行- 前進 (Progress)
任意在自己臨界區外執行的進程都不能中斷任何嘗試進入自己臨界區的進程- 有限等待 (Bounded waiting)
任意一個進程都不應該永遠等待進入自己的臨界區
Peterson 算法
Peterson 算法就很好的解決了競爭條件的問題,且也都滿足臨界區的三條原則。先來看看在 Peterson 算法中的進程結構。
首先得先假設有進程 Pi,Pj。然後有一個 bool 類型的 Peterson 數組,進程 i,j 對應在 Peterson 數組中有各自的值,當 Peterson[i] = TRUE 時就表示進程 i 想要嘗試進入臨界區。還有一個整型變量 turn,當 turn = i 時,就表示現在輪到進程 i 進入臨界區了。
// 進程共享資源
bool Peterson[2] = {
FALSE, FALSE}
int turn = i // 默認由 Pi 開始執行
// Pi進程結構
do {
// 進入區 start
Peterson[i] = TRUE
turn = j
while(Peterson[j] && turn == j);
// 進入區 end
// 臨界區 start
{
臨界區代碼 critical section }
// 臨界區 end
Peterson[i] = FALSE
// 剩餘區 start
{
剩餘區代碼 remainder section }
// 剩餘區 end
}while(TRUE)
// Pj進程結構
do {
// 進入區 start
Peterson[j] = TRUE
turn = i
while(Peterson[i] && turn == i);
// 進入區 end
// 臨界區 start
{
臨界區代碼 critical section }
// 臨界區 end
Peterson[j] = FALSE
// 剩餘區 start
{
剩餘區代碼 remainder section }
// 剩餘區 end
}while(TRUE)
那接下來就來分析 Peterson 進程結構:
首先可以看到,以 Pi 來說,當然希望可以進入自己的臨界區,所以要先將 Peterson[i] = TRUE
,但是這時不能直接進入 Pi 的臨界區,我們還要先檢查下 Pj 是否也想進入自己的臨界區並且也確實正在自己的臨界區中。
這邊的做法就是,先將 turn = j
,表示說 Pi 將進入臨界區的這個權利交給 Pj。然後利用 while 循環中的條件, while(Peterson[j] && turn == j);
,意思就是說如果此時 Pj 想要進入臨界區也確實輪到他(剛剛 Pi 把進入臨界區權力交給 Pj 就是為了這邊要檢查),那麼 Pi 就相當於死在 while 中,無法進入自己的臨界區,直到 Peterson[j]
或 turn == j
其中一個條件不符合才結束循環。而 Peterson[j]
可以在 Pj 執行完自己的臨界區後,由 Peterson[j] = FALSE
置
為 FALSE。
可以看到,通過 Peterson[] 和 turn 的鎖機制,Peterson 進程結構很好的確保了不會同時有多個進程進入自己的臨界區。而臨界區的代碼,應該就是那些我們希望不能同時被多個進程訪問或操作的東西,這樣就很好的起到了保護和隔離的作用。
以最開始的例子來說,如果今天我們把 P1,P2 累加 COUNT 該操作的語句放在各自的臨界區中,那就可以很好的起到保護的作用,不可能出現明明加了兩次,但是 COUNT 最後還是 1 的窘境了。