自己動手實現自旋鎖


注:本文部分內容來源於<<操作系統概念>>第六版,[美]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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM