自己動手實現自旋鎖

來源:互聯網
上載者:User

註:本文部分內容來源於<<作業系統概念>>第六版,[美]Abraham Silberschatz,Peter Baer Galvin,Greg Gagne著,鄭扣根譯。如有錯誤,還望大家批評指正,我先謝過大家了。

鎖是為瞭解決某種資源(又有人稱臨界資源)互斥使用提出的一種機制。常用的有讀寫鎖、互斥鎖、自旋鎖。接下來就談談這個自旋鎖。自旋鎖和互斥鎖功在使用時差不多,每一時刻只能有一個執行單元佔有鎖,而佔有鎖的單元才能獲得臨界資源的使用權,從而達到了互斥的目的。

自旋鎖與互斥鎖的區別在於:自旋鎖在執行單元在擷取鎖之前,如果發現有其他執行單元正在佔用鎖,則會不停的迴圈判斷鎖狀態,直到鎖被釋放,期間並不會阻塞自己。由於在等待時不斷的"自旋",這也是它為什麼叫做自旋鎖。所以自旋鎖使用時,是非常消耗CPU資源的。而互斥鎖在執行單元等待鎖釋放時,會把自己阻塞並放入到隊列中。當鎖被釋放時,會喚醒隊列上執行單元把其放入就緒隊列中,並由調度演算法進行調度並執行。所以互斥鎖使用時會有進程的環境切換,這可能是非長耗時的一個操作,但是等待鎖期間不會浪費CPU資源。所以對兩種鎖的使用必須要酌情處理。

現在我們自己來實現自旋鎖,即軟體層級的自旋鎖。

首先介紹幾個概念:

進入區:實現鎖請求的程式碼片段(紅色代碼)
臨界區:互斥執行的程式碼片段   
退出區:釋放鎖的程式碼片段      (紫色代碼)
剩餘區:其他程式碼片段

有N個進程{p0, p1, p2,......,pn}。

現在我們來說說最簡單情況,當執行單元(即進程)數是2的時候如何做。以下用進程來表示獨立的執行單元。首先想到可以用一個共用變數turn來表示當前由哪個進程執行。進程i的代碼結構如下,其中j=1-i。

do{    while(turn != i);  //進入區    //臨界區......    turn = j;          //退出區    //剩餘區......}while(1);

仔細觀察這段代碼會發現如下問題:turn的初始值決定了進程的執行順序。如果turn初始值為0,那麼進程1在進程0執行之前是不會獲得機會執行的。所以假如進程0壓根不想執行,那麼即使進程1干著急也必須得等。turn=1時也有這樣的問題。並且進程0與進程1嚴格交替執行,中間如果有誰不再執行,那麼另一個將也不再執行。我們也說這樣的實現不滿足有限等待性。即一個進程會總也得不到機會執行。

接下來考慮另一個實現方案:設定共用變數 boolean flag[2], 初始值為false。進程i的代碼結構如下,其中j=1-i。

do{    flag[i] = true;    //進入區    while(flag[j]);    //臨界區......    flag[i] = false;    //剩餘區......}while(1);

這段代碼滿足有限等待性的要求,即如果進程0不執行,進程1也可以執行。但是由於兩個進程是並發執行,所以可能會有如下的執行過程:
p0: flag[0]=true;
p1: flag[1]=true;
p0: while(flag[1]);
p1: while(flag[0]);

這樣的情況就是死結,即p0等待flag[1]變成false,p1等待flag[0]變成false。我們說這段代碼不滿足前進性。以上這兩段代碼確保每次只有一個進程能夠執行臨界區內的代碼,所以都滿足互斥性。

要證明一個演算法實現正確與否必須要證明代碼是否滿足一下三個性質:

1: 互斥性 //每次只有一個進程進入臨界區
2: 前進性 //即不會出現死結
3: 有限等待性 //即一個進程不會無限等待而得不到機會執行

第一個解決兩進程互斥問題的正確的軟體解決方案是由荷蘭數學家T.Dekker提出來的,也叫做Dekker演算法。Dekker演算法中進程i的代碼如下,其中j=1-i。兩進程共用boolean flag[2] 和 turn,flag初始化為false,而turn為0或1。

 1 do{ 2     flag[i]=ture;             //開始競爭 3     while(flag[j]){           //pj正在競爭 4         if(turn == j){        //應該輪到pj執行 5             flag[i] = false;  //主動放棄 6              while(turn == j); //pi等待pj釋放鎖 7             flag[i] = true;   //重新開始競爭 8         } 9     }10     //臨界區......11     turn = j;                 //主動退讓12     flag[i] = false;          //放棄競爭13     //剩餘區......14 }while(1);

互斥性:假設p0,p1都在臨界區,則flag[0]與flag[1]都為false。flag[0]在turn=1時才為false,flag[1]在turn=0時才為false。而p0,p1在臨界區之前都不會改變turn的值,所以turn在這之前只能有一個值,這說明turn即是0又是1,顯然不可能。所以p0與p1隻有一個會執行臨界區代碼。

前進性:初始化flag都為false,所以不參加競爭的進程不會影響參加競爭的進程。又由於turn每次只能有一個值,所以總會有一個進程主動放棄競爭,不會產生死結,從而另一個進程得到執行。

有限等待:假設p0正在臨界區而p1正在等待(第6行代碼),則p0會在p1第7行代碼執行結束之後最多執行一次,這樣p0就會有機會執行。更有意思的是,該演算法雖然滿足有限等待,但是並不能精確計算出要等待另一個進程執行多少次之後才能執行。原因在於p1在第7行代碼執行完之前p0可能執行了很多次。

我們看到這個演算法雖然正確,但是要想證明卻有些困難。其實,關於兩進程最簡單解法是有Peterson在1981年提出的。演算法代碼如下,其中j=1-i。

do{    flag[i] = true;         //開始競爭    turn = j;               //主動推讓    while(flag[j]&&turn==j);//等待    //臨界區......    flag[i]=false;          //放棄競爭    //剩餘區......}while(1);

互斥性:假設p0,p1都在臨界區,則turn即等於0又等於1,顯然不可能。故只有一個進程會進入臨界區。

前進性:因為turn每次只有一個值,故一定會有一個進程while迴圈不成立,不會死結。

有限等待:假設p1進入臨界區,p0正在等待且turn=1。p1執行完之後在下一次競爭之前會主動推讓,從而p0有機會執行。p0最多等待p1執行一次之後就會執行。

現在討論一下多個進程的解法,多進程的解法要比2進程解法複雜的多。Dijkstra在1965年給出了第一個有關n個進程互斥問題的解決方案,可是在這個方法裡,沒有給出一個進程在被允許進入臨界區以前必須等待的次數上限。隨後Knuth在1966年給出了第一個有限制的演算法,它的限制是2的n次方。隨後deBrujin改進了Knuth演算法,將等待次數減少到n^2。後來Eisenberg和McGuire成功將次數減少到n-1。Lamport則開發了最著名的麵包店演算法,它的等待次數也是n-1。

下面我們說說這個麵包店演算法。麵包店演算法的基本思想為:要進入臨界區的進程首先先要抽號,抽到最小號的進程進入臨界區,若有兩個進程抽到了相同的號,則進程編號最小的進程進入臨界區。

比較時比較的是一個數對(number[i], i),若(number[i], i) < (number[j], j),則pi進入臨界區。

(number[i], i) < (number[j], j) 等價於 (number[i] < number[j] || number[i] == number[j] && i < j)

N個進程共用 boolean choosing[N] , int number[N],初始化choosing為false, number為0。進程pi的代碼如下:

1:do{2:    choosing[i] = true;      //開始抽號3:    number[i] = max(number[0], number[1],......, number[N-1]);  //抽號4:    choosing[i] = false;     //抽號完畢5:    for(j = 0; j < N; ++j){6:        while(choosing[j]);  //如果有正在抽號的,則等待7:        while((number[j]!=0)&&((number[j], j)<(number[i],i))); //抽完號的進程如果有比自己小的,則等待8:    }9:    //臨界區......10:    number[i]=0; //抽到的號清0,下次重新抽號11:    //剩餘區......12:}while(1);

可以看到每個進程在進入臨界區之前必須先抽號,抽完號了並不能立刻進入臨界區,而是要和所有已經抽完號的進程進行比較,如果有比自己小的則自己通過while迴圈等待。

互斥性:假設pi, pk同時進入了臨界區。那麼此時number[i]和number[k]都不等於0。那麼此時(number[i], i)與(number[k],k)必能比較大小,且一定是一大一小。若(number[i], i)小,則pk陷入迴圈,否則pi陷入迴圈,這與兩個進程同時在臨界區矛盾。故互斥性成立。

前進性:由於(number[i], i) != (number[j], j) 當i!=j時,也就是說任意時刻,只有一個進程最小,從而保證至少有一個進程能夠執行,故不會出現死結。

有限等待:首先可以確定,不參加競爭的進程不會影響參加競爭的進程。並且假設首輪抽籤抉擇出了進程的執行順序。

p0, p1, p2,......p(n-1),當p0執行完之後,進行下一次抽號時,抽到的號一定會比已經抽完號的進程抽到的號大,故會在正在等待的進程執行結束之後才執行。
如下所示,p0, p1, p2 ,......., 代表初始的執行順序。每次一個進程執行結束之後重新抽號,那麼位置排序會放到右面。

p0, p1, p2, ......, p(n-1)|                 p0正在執行    p1, p2, ......, p(n-1)|p0               p0執行完畢並重新抽號,p1正在執行        p2, ......, p(n-1)|p0, p1           p1執行完畢並重新抽號,p2正在執行            p3,..., p(n-1)|p0, p1, p2       p2執行完畢並重新抽號,p3正在執行                 .        |     .                 .        |     .    第一選擇階段               第二選擇階段

第二選擇階段抽到的號普遍比第一選擇階段抽到的號大,故第一選擇階段的每個進程必都會有機會執行。而且最多等待n-1個進程執行完畢之後執行。所以有限等待性成立。

下面仔細說說這個演算法的每行代碼的意義:choosing初始化為false,number初始化為0
第6,7行代碼保證了:不爭奪臨界區的進程不影響其它進程的競爭。
第3行代碼保證了: 先抽號的進程抽到的號碼小,後抽號的進程抽到的號碼大,同時抽號的進程抽到的號碼一樣大。
第6行代碼保證了: 每次決策都是在所有競爭進程抽完號時進行的,保證公平。
第7行代碼保證了: 運行到臨界區的進程pi,必定(number[i], i)是最小的。

通過物件導向的封裝,很容易實現一個自旋鎖對象(spinlock),lock()即是進入區代碼, unlock()即是退出區代碼,具體細節大家自己設計思考,這個這裡就不多說了。

相關文獻:
Dijkstra[1965]:E.W.Dijstra, "Cooperating Sequential Processes", Technical Report, Technological University. Eindhoven,the Netherlands,1965, pages 43~112.
Peterson[1981]:G.L.Peterson, "Myths About the Mutual Exclusion Problem", Information Processing Letters, Volume 12,Number 3,1981.
Knuth[1966]:D.E.Knuth, "Additional Comments on a Problem in Concurrent Programming Control",Communications of the ACM,Volume 9, Number 5, 1966, pages 321~322.
deBruijn[1967]:N.G.deBruijn,"Additional Comments on a Problem in Concurrent Programming and Control", Communicaitonsof the ACM, Volume 10, Number 3, 1967, pages 137~138.
Eisenberg and McGuire[1972]M.A.Eisenberg and M.R.McGuire,"Future Comments on Dijkstra's Concurrent Programming ControlProblem", Communications of the ACM, Volume 15, Number 11, 1972, pages 999.
Lamport[1974]:L.Lamport, "A New Solution of Dijstra's Concurrent Programming Problem", Communication of the ACM, Volume
17, Number 8, 1974, pages 453~455.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.