Dekker互斥演算法詳解
大家好,這是本人的第一個技術部落格(也是第一個部落格),曾經看過《一個程式員的奮鬥史》,從而萌生了寫部落格的想法。本人目前正在自學嵌入式方向,是一個不折不扣的小鳥。這篇部落格亦來自於我學習電腦作業系統原理過程中的總結成果(視頻下載地址http://xidong.net/File001/File_53948.html)。如果有人也正在學習這方面內容,我非常希望能夠幫到你。如果有不對的地方歡迎大家指出和改正。
大多數系統允許多個進程共用資源(如CPU,IO裝置,硬碟等),而為了保證進程間能夠互不影響、安全正確地訪問這些共用資源,就必須對進程訪問共用資源採取某種控制。對於某一時刻僅允許一個進程訪問的共用資源就叫臨界資源,而訪問這些臨界資源的程式碼段就叫做臨界區。而電腦術語中,對進程排它地訪問臨界資源的這種控制手段就叫做互斥(也就是說某一時刻臨界區的進程只能為一個)。
解決進程互斥的方法有很多,比如軟體方法、硬體方法、訊號量方法、管程等方法,而今天我所說的Dekker互斥演算法就是軟體方法中的一種。
菜鳥設想:先讓我們看看一個初學者所能想到的最簡單的互斥訪問臨界區的虛擬碼:
初學者看見這段代碼,可能有人會覺得很奇怪。flag初始化為0,然後執行process方法的時候,while判斷不通過,故執行後面的代碼,而最後方法結束的時候,flag的值任然為0,這不是相當於while迴圈體永遠不會執行嗎。。。注意。。不能再用單進程編程那樣一段代碼從頭執行到底的“直線式”思維了。這裡可是多進(線)程編程。程式完全有可能在執行到任意一條語句時被中斷,從而跳到另外一個進程執行另外一段代碼。
上述代碼中,設定一個狀態值用於判斷進程是否可以進入臨界區,有程式進入臨界區的時候便將flag修改為忙碌,等到出臨界區的時候則重設為空白閑,從而釋放資源。表面上來看彷彿做到了互斥(當一個進程在臨界區時阻止了其他進程進入)。可是仔細看,其實上述代碼根本沒有達到互斥的功能。。。為什麼呢。先休息一下。等我談到改進設想1的時候再說原因。^_^
總之,上述的代碼並沒有達到我們所需要的功能,那麼我現在就推出一個很好的解決了互斥功能的代碼吧(僅僅是解決了互斥),這就是初步設想。
初步設想:代碼獻上:
注意到這段代碼,其實這段代碼的思路很清晰:將每個要訪問臨界區的進程設定一個序號,每個進程必須按照這個序號依次訪問臨界區。看上去貌似可以達到互斥。實際上的確如此,他完美的達到了互斥,僅此而已。程式嚴格地按照一定的次序(0->1->0->1...)訪問臨界區,從而當一個進程進入臨界區時,完全沒有留給任何進入臨界區的機會給其他進程。為什麼呢。同樣等到改進設想1的時候再說原因。^_^
基礎比較好的同學肯定會注意到:這樣做留下了好幾個要命的問題:首先訪問臨界區的兩個進程完全依賴彼此對number值的修改,假設一個進程在將序號值修改成其他進程需要的序號值之前,出現了錯誤直接退出進程,那麼序號將永遠得不到修改,那麼依賴這個序號值的進程將永遠得不到執行。這是很要命的,它違背了進程“有限等待、空閑讓進”的互斥原則。此外,當兩個進程訪問臨界資源的頻率不同時將會嚴重影響系統效率。比如,進程0每1分鐘要訪問臨界區,進程1每10分鐘訪問臨界區。當進程1退出臨界區後,十分鐘後才會進入臨界區,而進程0的每一分鐘就要進入臨界區,可是當進程0退出臨界區後必須等進程1退出臨界區後才能進入,而此時進程1並不需要進入臨界區,進程0必須在臨界區閒置情況下在9分鐘後等到臨界區被進程1訪問並退出後才能進入,而且每進行一個周期就要等一個9分鐘。也就是說訪問頻率低的進程嚴重影響了訪問頻率高的進程的執行效率,這明顯違背了“空閑讓進”原則。
可能菜鳥們(本人也是)對以上的表述;理解得不是太清楚。下面我來類比這樣一個例子:上面的情形就好像兩個人想進入一個僅有一個蹲位的廁所,門口有一個大爺將兩個人分別給了一個號碼牌(0、1),且規定必須按照“01010101...”的規則進入廁所。假如人A在上廁所的時候一個不小心將號碼牌0丟進了下水道,那麼人B就悲劇了,因為守門的大爺很奇怪,他永遠要等到號碼牌0從廁所裡出來才會讓人B進去。。。。另外一鐘情況,假如人A每一天上一次廁所,而人B腎功能不太好每5分鐘上一次廁所,那麼等人A上完廁所後,人B也第一次上完廁所後。。。天啊。他不給憋死才怪。
這下大家應該知道這種制度的可恨之處了吧,介於初步設想有這麼多弊端,下面進行了第一次改進。
改進設想1:代碼如下:
改進設想1取消了初步設想的序號訪問方式,使得上面的大部分問題得到解決。此方法其實與菜鳥設想大同小異,它們給了臨界區一個狀態標識,當標識為空白閑則允許進程進入,使得臨界區得到充分利用,不同地方在於改進設想1狀態資訊更為精確,它使得程式知道是哪個進程佔用了臨界區,加大了編程的靈活性,但是卻限制了互斥演算法的使用條件:僅能控制兩個進程進行互斥操作,其實這也是Dekker演算法的缺點之一。
不幸的是,改進設想1與菜鳥設想一樣,同樣不能實現真正的互斥,這到底是為什麼呢。我前面提到,多進程的並發具有不確定性,程式執行過程中可能會不停地進行進程切換。讓我們看一下這樣一種情形:進程0一路執行完(1)處,由於flag[1]被初始化為0,因而跳過迴圈體,準備開始執行(2),此時進程突然被切換,轉而執行進程1,等到進程1執行到(3)時,因為(2)處還未得到執行,flag[0]的值還沒有來的修改,還是初始值0,結果進程1同樣繞過了迴圈體順利的進入了臨界區,這樣就導致了進程0和進程1同時進入了臨界區,使得互斥失效。同樣的道理菜鳥設想也犯了這個致命的錯誤。然而為什麼序號訪問就能順利的互斥呢。這是因為無論如何切換,number的值不是0就是1,這就決定了兩個while迴圈(一個判斷是否為0,一個判斷是否為1)只有一個能順利“繞過”迴圈體,並進入臨界區,從而達到互斥訪問。
回到剛才我類比的例子,就好比此時廁所上裝了一個號誌,用來表示廁所裡是否有人,當廁所外面的人看見號誌亮則表示裡面有人而不能進廁所,反之則表示無人可進。這時候假如人A看見號誌是黑的,進去之後還沒來得急按下號誌的開關,而此時恰好人B又看見燈是黑的,於是也進入了廁所,這時,就尷尬了。。。
可見由於進程的切換的不確定性導致了進程同時進入了臨界區,而上述情況的原因在於修改狀態的“時機”不對,因此就有了改進設想2。
改進設想2:代碼如下:
改進設想2將狀態的修改放在了迴圈之前,即進程訪問臨界區時需提前表達訪問臨界區的“意願”,這樣一來,無論進程如何切換等到其中一個進程開始執行迴圈條件判斷時,要麼僅有一個flag為0,要麼兩個均為1,這就保證了兩個進程無法同時繞過迴圈體。可是細心的人會發現這樣一個問題:當進程0一路執行完(1)處,由於flag[1]被初始化為0,因而跳過迴圈體,準備開始執行(2),此時進程若被切換至進程1,等進程1執行完(3)時,又被切換回進程0執行,而此時flag[1]被修改為1滿足迴圈條件進入迴圈體,在迴圈的某一時刻又切換到(4)執行,同樣由於flag[0]被修改為1進入迴圈體,結果可想而知,兩個進程都進入了迴圈體而不能修改自己狀態值,導致兩個進程永遠都不能跳出迴圈。這種情形在電腦中叫做“死結”,除非某一進程主動放棄資源,或系統主動幹涉,否則進程永遠佔用資源但卻又得不到推進。
用廁所的類比來說,此種方法就是給了每個人一個遙控器,使之能在進入廁所之前就能夠遠程點亮號誌,從而避免同時進入廁所,可是倘若人A點亮了屬於自己的號誌,但還沒來得及進入廁所時,人B也不甘示弱的點亮了自己的號誌,兩個人都互不謙讓,從而造成誰也進不了廁所。。。
可以看出上述問題出現的根結所在,由於兩個進程均提前表達了進入臨界區的企圖,可是誰也不肯放棄自己進入臨界區的意願,互不退讓,並且不斷查看狀態值,從而導致死結。
改進設想3:代碼如下:
改進的思路很簡單,讓進程在發現對方進入想臨界區的願望後,將自己的意願停止一個隨機時間長度,從而達到相互謙讓地表達自身的意願。這樣一來即使雙發進程在某一刻因為對方的狀態值均為1而都進入了while迴圈體,可是由於進程將會放棄進入意願而使得某一進程總會因為不滿足迴圈條件而跳出迴圈,從而進入臨界區。由於停止時間是隨機的,或許會經過很多次迴圈判斷的時候對方的狀態都為1,可是,總會有一時刻
使得兩個進程的執行進度會得到錯開,使得雙方進行迴圈判斷的時候不會兩個flag都為1。雖然此種方法不會進入死結,可是卻存在一種發生可能性非常低的僵局,即雙方在謙讓過後可能同時又進行迴圈判斷,從而一次又一次的謙讓下去,導致程式效率降低,甚至出現雙方的謙讓導致兩個進程的阻塞時間接近於無窮大的情況。
再用到廁所的例子來說明:人A與人B看見對方進入廁所的號誌都是亮著的,便都主動禮貌地謙讓,關掉了自己的號誌,可是呢,兩人實在過於默契,兩人都在同一時刻關燈,並且又在同一時間開燈,雙發就這麼一直讓來讓去,兩人就這麼僵持著很長一段時間,誰也沒能進入臨界區。。。
然而,程式設計必須保持絕對的嚴謹,雖然在停止了一個隨機時間的情況下,兩個進程仍然保持“同步推進”的情況實在微乎其微,但是即使有%0.00...001的可能性使得程式死結,那麼這個設計便是不完美的。為了徹底杜絕決形成這種僵局的可能性,便誕生了最終演算法也就是大名鼎鼎的Dekker互斥演算法。(抱歉,這一段,實在不知道該怎麼表達才通俗易懂)
最終設想(Dekker互斥演算法):
Dekker演算法終於出爐了,可以看出Dekker演算法是基於改進設想3的謙讓思路的基礎上,採取的一種“謙讓而又不過分謙讓”的折中思想。如何避免雙方不停的謙讓呢。Dekker演算法採用了初步設想的序號訪問思想,使得進入迴圈的進程按照序號來決定到底該不該一直謙讓,有人會問,採用序號的是否使得進程出現按照序號訪問臨界區的老毛病呢。答案是不會的,至少是不會嚴格的按照序號輪流訪問臨界區,原因在於,按照序號來謙讓有一個前提條件:兩個進程必須因為對方的狀態值恰好都為忙碌而同時進入迴圈體,而在此之前,兩個進程進入臨界區的機率是相同的,跟序號是沒有關係的。也就是說想要序號訪問臨界區還需要一點“運氣”。
總之,Dekker演算法首先使用狀態值的方式解決了序號訪問臨界區的弊端,又利用改進思想2使得進程可以互斥的訪問臨界資源,同時又採用了改進思想3的方法避免了死結現象,而最後結合了序號謙讓方式解決了因為避免死結而產生的僵局現象,就這樣循序漸進地用軟體方法解決了進程的互斥的問題。
然而,Dekker方法就真是完美無缺的嗎。很遺憾,就算是Dekker演算法也無法避免軟體互斥方法的一個通病,那就是忙等現象。大家注意以上的每一個設想中,都不可或缺地使用while迴圈,而迴圈體中,執行地都是毫無意義的代碼,也就是說軟體方法利用無意義的迴圈使得進程無法向前推進來達到阻塞進程的目的,然而讓CPU去執行無意義的代碼本身就是一種嚴重資源浪費,進程既佔用了CPU,然而卻沒有生產任何有效資料,可以說,這樣做嚴重降低了CPU的效率。這是整個軟體方法都無法迴避的問題。
此外,大家可以看見Dekker演算法僅能進行兩個進程的互斥,對於兩個以上的互斥問題,實現起來相當複雜。
並且,軟體方法在實現互斥的時候,需要相當小心,一會不小就是互斥失敗啦,死結啦,之類的。
第一篇部落格終於寫完了,寫完之後總覺得是否太囉嗦了,而且有些地方總覺得沒解釋太清楚,看來有些事情真的只可意會不可言傳啊,如果有讀者覺得有哪裡不對,或者需要補充什麼,歡迎來噴。謝謝。