“我找到好辦法了!”
沒有想到,說話的人竟然是磁盤!
進程調度器瑟瑟的說:“你有方法?還是算了吧,我怕用你的方法操作系統要亂套了。”
磁盤委屈的道:“不就是剛剛冤枉你了嗎,這么小氣干什么!再說了,這個方法不是我想出來的,是我從文件里找到的。”
操作系統挑了挑眉毛:“哦?你找到什么文件了,讓大家也瞅瞅?”
磁盤嗡嗡的轉起來,很快就把文件取出來了。
“當當當當~ 這可是大師 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 環境高級編程」這本書里寫到「。。。三種特性造成了這種並非必要的復雜性」,對於一般的互斥操作,還是建議使用互斥鎖(當然是阻塞而非忙等待)。稍微復雜點的鎖還有「讀寫鎖」,以后有機會再講吧~
覺得我寫的還不錯的話,就點個贊吧!