1. 程式人生 > >互斥那點事兒(下)

互斥那點事兒(下)

“我找到好辦法了!”

沒有想到,說話的人竟然是磁碟!

程序排程器瑟瑟的說:“你有方法?還是算了吧,我怕用你的方法作業系統要亂套了。”

磁碟委屈的道:“不就是剛剛冤枉你了嗎,這麼小氣幹什麼!再說了,這個方法不是我想出來的,是我從檔案裡找到的。”

作業系統挑了挑眉毛:“哦?你找到什麼檔案了,讓大家也瞅瞅?”

磁碟嗡嗡的轉起來,很快就把檔案取出來了。

“噹噹噹當~ 這可是大師 Dijkstra 的論文,他引入了一個全新的變數型別——訊號量(semaphore)。然後還為訊號量設定了兩種操作,P(proberen,檢測) 和 V(verhogen,增量) 。”

”說清楚點啊,訊號量是怎麼個用法啊?“程序急切的問道。

“別急,讓我接著看。。。Dijkstra 提出,P操作是檢測訊號量是否為正值,如果不是,就阻塞呼叫程序。 V操作能喚醒一個阻塞程序,讓他恢復執行 。具體點的話就是這樣: “

// S 為訊號量
P(s):
{
S = S - 1
if (S < 0)
    {
        呼叫該 P 操作的程序阻塞,並插入相應的阻塞佇列;
    }
}
// S 為訊號量
V(s):
{
S = S + 1
if (S <= 0)
    {
        從等待訊號量 S 的阻塞佇列裡喚醒一個程序;
    }
}

記憶體仔細看了程式碼,說:”這個實現也要求是原子操作誒,Dijkstra 這個方法很有趣啊。“

程序蒙圈了:“我怎麼完全看不懂啊?記憶體你給我講講唄。”

“好,我就用最簡單的一組執行緒舉例子了:

// 執行緒 A,B,C , S = 1
...
P(S)        //S = S - 1  若 S < 0 ,阻塞等待
購票操作
V(S)        //S = S + 1  若 S <= 0, 表明有執行緒阻塞了,得喚醒其中一個 
...

這裡的 「購票操作」 就是我們要保護的臨界區,我們要保證一次只能有一個執行緒進入。那我們就把 S 的初始值設為 1 。當執行緒 A 第一個呼叫 P(S) 後,S 的值就變成了 0 ,A 成功進入臨界區。在 A 出臨界區之前,執行緒 B 如果呼叫 P(S)

, S 就變成 -1 ,滿足 S < 0 的判斷條件,執行緒 B 就被阻塞了。等 A 呼叫 V(S) 後,S 的值又變成 0 ,滿足 S <= 0,就會把執行緒 B 喚醒,B 就能進入臨界區了。“

程序恍然大悟:“原來是這樣,看起來和二元鎖差不多啊,但是不用忙等待了。”

記憶體神祕一笑:“訊號量能做的可不止這些,你想想看,要是我把 S 的初始值設為 2 ,會發生什麼?”

“一次能有兩個執行緒訪問臨界區!”程序這次反應快多了:“也就是說 S 的初始值可以控制有多少個執行緒進入臨界區,太厲害了!”

tobe 注:從訊號量的值能看出還有多少個程序能進入臨界區,如果為負數,表明有 x 個程序因為呼叫 P(S) 而被阻塞

“沒錯,所以說訊號量是一個很靈活的併發機制。而且訊號量還有另一個厲害的用處:

你看這兩個程序有什麼特別的地方?“

“emmmm,這個嘛,程序 P2 的 V 操作居然放在 P 操作的前面,而且兩個操作的訊號量還不是同一個。”

“沒錯,這樣使用訊號量,能讓兩個程序做到同步。你看,如果 P1 執行到 P(S1),他是不是會阻塞?”

程序認真一看,說:“沒錯誒,S1 初始值是 0,P1 肯定得停在這一句。讓我再看看,,,如果 P1 想接著執行,就得等 P2 呼叫 V(S1) 把他喚醒。”

“是的,這就是同步——執行快的 P1 必須在這裡停下來等 P2 執行到指定位置。兩個程序的執行順序就是這樣:

也就是說 x 最終的值必然是 30,而不可能是 20。在訊號量的幫助下,這兩個程序達成了同步。“

程序由衷的感嘆:“訊號量實在是太強大了!咱們以後就用訊號量來解決互斥的問題吧!”


tobe 注:在 Linux 裡提供了訊號量和互斥量(也就是二元鎖)這兩種主要機制實現互斥,不過 Linux 的訊號量功能要比文章裡講得複雜得多,「UNIX 環境高階程式設計」這本書裡寫到「。。。三種特性造成了這種並非必要的複雜性」,對於一般的互斥操作,還是建議使用互斥鎖(當然是阻塞而非忙等待)。稍微複雜點的鎖還有「讀寫鎖」,以後有機會再講吧~

覺得我寫的還不錯的話,就點個贊吧