Mutex的實現(xiàn)原理
掃描二維碼
隨時隨地手機看文章
推薦閱讀這份資料:OSTEP鎖章節(jié)(https://pages.cs.wisc.edu/~remzi/OSTEP/threads-locks.pdf)
這份文檔是關于并發(fā)編程中鎖(Locks)的詳細介紹和討論。文檔從鎖的基本概念出發(fā),探討了如何在多線程環(huán)境中保護共享資源,避免競態(tài)條件。以下是文檔的詳細翻譯:
鎖:基本概念
在并發(fā)編程中,我們希望一系列指令能夠原子性地執(zhí)行,但由于單個處理器上的中斷存在(或者多個線程在多個處理器上并發(fā)執(zhí)行),我們無法實現(xiàn)。本章直接解決這個問題,介紹了鎖的概念。程序員在源代碼中使用鎖來標注臨界區(qū),確保這些臨界區(qū)的執(zhí)行就像是單個原子指令一樣。
舉例來說,假設我們的臨界區(qū)是這樣的,對共享變量的典型更新:
balance = balance + 1;
當然,還有其他可能的臨界區(qū),比如向鏈表添加元素或其他更復雜的共享結構更新,但我們現(xiàn)在只保持這個簡單的例子。使用鎖,我們在臨界區(qū)周圍添加一些代碼,如下所示:
lock_t mutex; // 一些全局分配的鎖 'mutex' ... lock(&mutex); balance = balance + 1; unlock(&mutex);
鎖只是一個變量,因此要使用它,你必須聲明某種類型的鎖變量(如上面的mutex)。這個鎖變量(或簡稱"鎖")保存了鎖在任何時刻的狀態(tài)。它要么是可用的(或未鎖定或空閑),這意味著沒有線程持有鎖,要么是已獲取的(或鎖定或持有),這意味著恰好有一個線程持有鎖,并且假定它處于臨界區(qū)。我們還可以在數(shù)據(jù)類型中存儲其他信息,比如哪個線程持有鎖,或者一個用于鎖定獲取順序的隊列,但這些信息對鎖的用戶是隱藏的。lock()和unlock()例程的語義很簡單。調用lock()例程嘗試獲取鎖;如果沒有其他線程持有鎖(即它是空閑的),線程將獲取鎖并進入臨界區(qū);這個線程有時被稱為鎖的擁有者。
如果另一個線程然后對同一個鎖變量(本例中的mutex)調用lock(),它將在鎖被另一個線程持有時不會返回;這樣,其他線程就被阻止在第一個持有鎖的線程在里面時進入臨界區(qū)。一旦鎖的擁有者調用unlock(),鎖現(xiàn)在又可用(空閑)了。如果沒有其他線程在等待鎖(即沒有其他線程調用lock()并卡在那里),鎖的狀態(tài)就簡單地變?yōu)樽杂伞?
如果有等待線程(卡在lock()中),它們中的一個將(最終)注意到(或被告知)鎖狀態(tài)的變化,獲取鎖,并進入臨界區(qū)。鎖為程序員提供了一些最基本的調度控制。一般來說,我們認為線程是由程序員創(chuàng)建但由操作系統(tǒng)調度的實體,以操作系統(tǒng)選擇的任何方式。鎖將其中一些控制權還給了程序員;通過在代碼段周圍放置一個鎖,程序員可以保證不會有多個線程同時在該代碼中活躍。因此,鎖幫助將傳統(tǒng)的操作系統(tǒng)調度混亂轉變?yōu)楦锌刂频幕顒印?
Pthread 鎖
POSIX庫中鎖的名稱是互斥鎖(mutex),因為它用于在多個線程之間提供互斥,即如果一個線程處于臨界區(qū),它會排除其他線程進入,直到它完成該部分。因此,當你看到以下POSIX線程代碼時,你應該理解它與上面做的是同一件事(我們再次使用我們的包裝器,在鎖和解鎖時檢查錯誤):
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; ... Pthread_mutex_lock(&lock); // 包裝器;在失敗時退出 balance = balance + 1; Pthread_mutex_unlock(&lock);
你可能還會注意到這里POSIX版本傳遞了一個變量來鎖定和解鎖,因為我們可能使用不同的鎖來保護不同的變量。這樣做可以增加并發(fā)性:而不是使用一個大鎖(粗粒度鎖定策略),每當任何臨界區(qū)被訪問時,我們通常會用不同的鎖來保護不同的數(shù)據(jù)和數(shù)據(jù)結構,從而允許更多的線程同時處于鎖定代碼中(更細粒度的方法)。
構建一個鎖
到目前為止,你應該已經(jīng)從程序員的角度理解了鎖的工作原理。但我們應該如何構建一個鎖?需要什么硬件支持?需要什么操作系統(tǒng)支持?在本章的其余部分,我們將解決這個問題。
關鍵問題:如何構建一個鎖?有效的鎖以低成本提供互斥,并且可能獲得我們在下面討論的其他一些屬性。需要什么硬件支持?需要什么操作系統(tǒng)支持?
要構建一個工作的鎖,我們將需要我們老朋友硬件的幫助,以及我們好朋友操作系統(tǒng)。多年來,各種計算機架構的指令集中添加了許多不同的硬件原語;雖然我們不會研究這些指令是如何實現(xiàn)的(畢竟,那是計算機體系結構課程的主題),但我們將研究如何使用它們來構建像鎖這樣的互斥原語。我們還將研究操作系統(tǒng)如何參與其中,完成整個畫面,使我們能夠構建一個復雜的鎖定庫。
評估鎖
在構建任何鎖之前,我們應該首先理解我們的目標是什么,因此我們問如何評估特定鎖實現(xiàn)的有效性。評估鎖是否有效(并且工作得很好),我們應該建立一些基本標準。第一個是鎖是否完成了它的基本任務,即提供互斥。
基本上,鎖是否有效,防止多個線程進入臨界區(qū)?第二個是公平性。每個爭奪鎖的線程在鎖空閑時都有公平的機會獲取它嗎?另一種看待這個問題的方式是通過檢查更極端的情況:是否有線程在爭奪鎖時餓死了,因此從未獲得它?最后的標準是性能,特別是使用鎖增加的時間開銷。這里有幾個值得考慮的不同情況。
一個是無爭用的情況;當單個線程運行并獲取和釋放鎖時,這樣做有什么開銷?
另一個是多個線程在單個CPU上爭奪鎖的情況;在這種情況下,是否有性能問題?最后,當有多個CPU參與,每個CPU上的線程都在爭奪鎖時,鎖的性能如何?通過比較這些不同的場景,我們可以更好地理解使用各種鎖定技術的性能影響,如下所述。
控制中斷
最早用于提供互斥的解決方案之一是為臨界區(qū)禁用中斷;這種解決方案是為單處理器系統(tǒng)發(fā)明的。代碼如下所示:
void lock() { DisableInterrupts(); } void unlock() { EnableInterrupts(); }
假設我們運行在這樣的單處理器系統(tǒng)上。通過在進入臨界區(qū)之前關閉中斷(使用某種特殊的硬件指令),我們確保臨界區(qū)內的代碼不會被中斷,因此將像原子操作一樣執(zhí)行。當我們完成時,我們重新啟用中斷(再次通過硬件指令),因此程序照常進行。
這種方法的主要優(yōu)點是其簡單性。你肯定不必太費勁就能弄清楚為什么這有效。沒有中斷,線程可以確信它執(zhí)行的代碼將被執(zhí)行,沒有其他線程會干擾它。不幸的是,缺點很多。首先,這種方法要求我們允許任何調用線程執(zhí)行特權操作(打開和關閉中斷),因此相信這種設施不會被濫用。正如你已經(jīng)知道的,任何時候我們被要求相信任意程序,我們可能都有麻煩了。
在這里,麻煩以多種方式表現(xiàn)出來:一個貪婪的程序可以在執(zhí)行開始時調用lock(),從而壟斷處理器;更糟的是,一個出錯或惡意的程序可以在調用lock()后進入無限循環(huán)。在這種情況下,操作系統(tǒng)永遠不會重新獲得系統(tǒng)的控制權,只有一個解決辦法:重新啟動系統(tǒng)。使用中斷禁用作為通用同步解決方案需要對應用程序過于信任。
其次,這種方法在多處理器上不起作用。如果多個線程在不同的CPU上運行,并且每個都試圖進入同一個臨界區(qū),不管是否禁用了中斷;線程將能夠在其他處理器上運行,因此可能進入臨界區(qū)。由于多處理器現(xiàn)在已經(jīng)很常見,我們的通用解決方案將不得不做得比這更好。第三,長時間關閉中斷可能導致中斷丟失,這可能導致嚴重的系統(tǒng)問題。例如,想象一下,如果CPU錯過了磁盤設備完成讀請求的事實。操作系統(tǒng)將如何知道喚醒等待所述讀取的進程?
一個失敗的嘗試:僅使用加載/存儲
要超越基于中斷的技術,我們將不得不依賴CPU硬件和它為我們提供的指令來構建一個合適的鎖。讓我們首先嘗試使用一個標志變量來構建一個簡單的鎖。在這個失敗的嘗試中,我們將看到構建鎖所需的一些基本思想,并(希望)看到為什么僅使用單個變量并通過正常的加載和存儲訪問它是不夠的。在這個第一次嘗試(圖28.1)中,想法非常簡單:使用一個簡單的變量(標志)來指示某個線程是否擁有鎖。第一個進入臨界區(qū)的線程將調用lock(),它測試標志是否等于1(在這種情況下,不是),然后將標志設置為1,以表明該線程現(xiàn)在持有鎖。當完成臨界區(qū)后,線程調用unlock()并清除標志,從而表明鎖不再被持有。
如果另一個線程恰好在第一個線程在臨界區(qū)時調用lock(),它將簡單地在while循環(huán)中自旋等待,等待該線程調用unlock()并清除標志。一旦第一個線程這樣做,等待的線程將退出while循環(huán),為自己將標志設置為1,并繼續(xù)進入臨界區(qū)。不幸的是,代碼有兩個問題:一個是正確性問題,另一個是性能問題。一旦你習慣了并發(fā)編程的思考,正確性問題就很容易看到。想象一下圖28.2中的代碼交錯;假設標志=0開始。正如你從這個交錯中看到的,通過及時(不合時宜?)的中斷,我們很容易產(chǎn)生一個情況,兩個線程都設置標志為1,因此兩個線程都能夠進入臨界區(qū)。這種行為是專業(yè)人士所說的"糟糕"——我們顯然未能提供最基本的要求:提供互斥。
我們將在后面更詳細地解決性能問題,即線程等待獲取已經(jīng)持有的鎖的方式:它不斷地檢查標志的值,這種技術被稱為自旋等待。自旋等待浪費了等待另一個線程釋放鎖的時間。在單處理器上,浪費尤其高,因為等待的線程等待的線程甚至不能運行(至少,直到上下文切換發(fā)生?。R虼?,隨著我們向前發(fā)展并開發(fā)出更復雜的解決方案,我們也應該考慮避免這種浪費的方法。
使用測試和設置構建工作的自旋鎖
因為禁用中斷在多個處理器上不起作用,而且簡單的使用加載和存儲的方法(如上所示)也不起作用,系統(tǒng)設計者開始發(fā)明鎖定的硬件支持。最早的多處理器系統(tǒng),如1960年代初的Burroughs B5000 [M82],有這樣的支持;今天所有系統(tǒng)都提供這種類型的支持,即使是單CPU系統(tǒng)。最簡單的硬件支持是眾所周知的測試和設置(或原子交換1)指令。我們通過以下C代碼片段定義測試和設置指令的作用:
int TestAndSet(int *old_ptr, int new) { int old = *old_ptr; // 獲取old_ptr處的舊值 *old_ptr = new; // 將'new'存儲到old_ptr return old; // 返回舊值 }
每個支持測試和設置的架構都用不同的名稱來稱呼它。在SPARC上,它被稱為加載/存儲無符號字節(jié)指令(ldstub);在x86上,它是原子交換(xchg)的鎖定版本。
評估自旋鎖
鑒于我們的基本自旋鎖,我們現(xiàn)在可以沿著我們之前描述的軸評估它的有效性。鎖最重要的方面是正確性:它是否提供互斥?答案是肯定的:自旋鎖只允許單個線程一次進入臨界區(qū)。因此,我們有一個正確的鎖。下一個軸是公平性。自旋鎖對等待線程有多公平?你能保證等待線程最終會進入臨界區(qū)嗎?不幸的是,這里的答案是壞消息:自旋鎖不提供任何公平性保證。事實上,一個自旋的線程可能會永遠自旋,在爭用中。簡單的自旋鎖(到目前為止討論的)是不公平的,可能導致饑餓。最后一個軸是性能。
使用自旋鎖的成本是什么?為了更仔細地分析這個問題,我們建議考慮幾種不同的情況。首先,想象線程在單個處理器上爭奪鎖;其次,考慮線程分布在許多CPU上。對于自旋鎖,在單個CPU的情況下,性能開銷可能相當痛苦;想象一下,持有鎖的線程在臨界區(qū)內被搶占。調度器可能會運行每個其他線程(想象有N-1個其他線程),每個線程都試圖獲取鎖。在這種情況下,這些線程中的每一個都會在時間片期間自旋,然后放棄CPU,浪費CPU周期。然而,在多個CPU上,自旋鎖工作得相當好(如果線程的數(shù)量大致等于CPU的數(shù)量)。
思考如下:想象線程A在CPU 1上,線程B在CPU 2上,都爭奪一個鎖。如果線程A(CPU 1)抓住了鎖,然后線程B嘗試,B將在(CPU 2上)自旋。然而,假設臨界區(qū)很短,很快鎖就可用,被線程B獲取。在這種情況下,等待另一個處理器上持有的鎖并不會浪費太多周期,因此可以是有效的。
比較和交換
一些系統(tǒng)提供的另一個硬件原語被稱為比較和交換指令(例如,在SPARC上),或比較和交換(在x86上)。這個單指令的C偽代碼可以在圖28.4中找到?;舅枷胧潜容^和交換測試ptr指定的地址處的值是否等于expected;如果是,用新值更新ptr指向的內存位置。
如果不是,什么也不做。在這兩種情況下,返回該內存位置的原始值,從而允許調用比較和交換的代碼知道它是否成功。有了比較和交換指令,我們可以以與測試和設置非常相似的方式構建鎖。例如,我們可以簡單地將上面的lock()例程替換為以下內容:
void lock(lock_t *lock) { while (CompareAndSwap(&lock->flag, 0, 1) == 1) ; // 自旋 }
其余代碼與上面的測試和設置示例相同。這段代碼的工作方式相當相似;它只是檢查標志是否為0,如果是,則原子地交換1,從而獲取鎖。試圖在持有鎖時獲取鎖的線程將被卡住自旋,直到鎖最終被釋放。
如果你想看看如何真正制作一個C可調用的x86版本的比較和交換,代碼序列(來自[S05])可能很有用2。最后,正如你可能已經(jīng)感覺到的,比較和交換是一個比測試和設置更強大的指令。我們將在未來簡要探討鎖無關同步[H91]等主題時利用這種力量的一些用途。然而,如果我們只是用它構建一個簡單的自旋鎖,它的行為與我們上面分析的自旋鎖相同。
加載鏈接和存儲條件
一些平臺提供了一對指令,它們協(xié)同工作以幫助構建臨界區(qū)。例如,在MIPS架構[H93]上,加載鏈接和存儲條件指令可以一起使用來構建鎖和其他并發(fā)結構。這些指令的C偽代碼可以在圖28.5中找到。Alpha、PowerPC和ARM提供了類似的指令[W09]。加載鏈接的操作與典型的加載指令非常相似,它只是從內存中獲取一個值并將其放入寄存器中。
存儲條件的關鍵區(qū)別在于,它只有在沒有對地址進行干預存儲的情況下才會成功(并更新剛剛從加載鏈接中加載的地址處的值)。在成功的情況下,存儲條件返回1并更新ptr處的值為value;如果失敗,ptr處的值不會更新,返回0。作為對自己的挑戰(zhàn),嘗試思考如何使用加載鏈接和存儲條件構建鎖。然后,當你完成時,看看下面的代碼,它提供了一個簡單的解決方案。去做吧!lock()代碼是唯一有趣的部分。首先,線程自旋等待標志被設置為0(從而表明鎖沒有被持有)。
一旦如此,線程嘗試通過存儲條件獲取鎖;如果成功,線程已經(jīng)原子地將標志的值更改為1,因此可以繼續(xù)進入臨界區(qū)。注意存儲條件失敗可能發(fā)生的情況。一個線程調用lock()并執(zhí)行加載鏈接,返回0,因為鎖沒有被持有。在它可以嘗試存儲條件之前,它被中斷,另一個線程進入鎖代碼,也執(zhí)行加載鏈接指令,
int LoadLinked(int *ptr) { return *ptr; } int StoreConditional(int *ptr, int value) { if (no update to *ptr since LL to this addr) { *ptr = value; return 1; // 成功! } else { return 0; // 更新失敗 } }
并且也得到0并繼續(xù)。此時,兩個線程都已經(jīng)執(zhí)行了加載鏈接,每個線程都即將嘗試存儲條件。這些指令的關鍵特性是,只有其中一個線程會成功更新標志為1并因此獲得鎖;第二個嘗試存儲條件的線程將失?。ㄒ驗榱硪粋€線程在其加載鏈接和存儲條件之間更新了標志的值),因此不得不嘗試再次獲取鎖。幾年前在課堂上,本科生David Capel提出了上述更簡潔的形式,供那些喜歡短路布爾條件的人使用??纯茨隳芊衽宄槭裁此葍r。它當然更短!
void lock(lock_t *lock) { while (LoadLinked(&lock->flag) || !StoreConditional(&lock->flag, 1)) ; // 自旋 }
獲取和添加
最后一個硬件原語是獲取和添加指令,它原子地遞增一個值,同時返回特定地址的舊值。獲取和添加指令的C偽代碼如下所示:
int FetchAndAdd(int *ptr) { int old = *ptr; *ptr = old + 1; return old; }
在這個例子中,我們將使用獲取和添加來構建一個更有趣的票鎖,由Mellor-Crummey和Scott [MS91]引入。鎖和解鎖代碼可以在圖28.7(第14頁)中找到。與單個值不同,這種解決方案結合了票和輪次變量來構建鎖?;静僮鞣浅:唵危寒斁€程希望獲取鎖時,它首先對票值進行原子獲取和添加;該值現(xiàn)在被認為是該線程的"輪次"(myturn)。
全局共享的lock->turn用于確定哪個線程的輪次;當(myturn == turn)對于給定線程時,就是該線程進入臨界區(qū)的時候。通過簡單地遞增turn來解鎖,以便下一個等待的線程(如果有)現(xiàn)在可以進入臨界區(qū)。
注意這個解決方案與我們之前的嘗試的一個重要區(qū)別:它為所有線程確保了進展。一旦線程被分配了它的票值,它將在未來的某個時候被調度(一旦前面的線程通過臨界區(qū)并釋放了鎖)。在我們之前的嘗試中,沒有這樣的保證;一個在測試和設置上自旋的線程(例如)可能會永遠自旋,即使其他線程獲取和釋放鎖。
太多自旋:現(xiàn)在怎么辦?
我們的基于硬件的鎖很簡單(只有幾行代碼),它們有效(如果你愿意,你甚至可以證明這一點,通過編寫一些代碼),這是任何系統(tǒng)或代碼的兩個極好的屬性。然而,在某些情況下,這些解決方案可能非常低效。
想象一下你在單個處理器上運行兩個線程?,F(xiàn)在想象一個線程(線程0)在臨界區(qū)內,因此持有鎖,不幸的是被中斷了。第二個線程(線程1)現(xiàn)在試圖獲取鎖,但發(fā)現(xiàn)它被持有。因此,它開始自旋。然后它再自旋。然后它再自旋一些。最后,定時器中斷發(fā)生,線程0再次運行,釋放了鎖,最后(下一次運行時,比如說),線程1不需要那么多自旋就能獲取鎖。
因此,任何時候線程被卡在自旋中,就像這樣,它都會浪費整個時間片,什么都不做,只是檢查一個不會改變的值!當N個線程爭奪鎖時問題變得更糟;N-1個時間片可能會以類似的方式被浪費,只是自旋等待一個線程釋放鎖。因此,我們接下來的問題:關鍵問題:如何避免在CPU上無謂地浪費時間自旋?
僅靠硬件支持無法解決問題。我們還需要操作系統(tǒng)支持!現(xiàn)在讓我們弄清楚這可能如何工作。
一個簡單的方法:只是讓步,寶貝
硬件支持讓我們走得相當遠:工作的鎖,甚至(如票鎖的情況)在鎖獲取中的公平性。然而,我們仍然有一個問題:當上下文切換發(fā)生在臨界區(qū)內,線程開始無休止地自旋等待被中斷的(持有鎖的)線程再次運行時,該怎么辦?我們第一次嘗試是一個簡單而友好的方法:當你即將自旋時,相反,放棄CPU讓另一個線程運行。正如Al Davis可能會說的,"只是讓步,寶貝!"[D91]。圖28.8(第15頁)展示了這種方法。
在這個方法中,我們假設操作系統(tǒng)原始的yield(),當線程想要放棄CPU并讓另一個線程運行時可以調用。線程可以處于三種狀態(tài)之一(運行、就緒或阻塞);yield只是一個系統(tǒng)調用,將調用者從運行狀態(tài)移動到就緒狀態(tài),從而提升另一個線程到運行狀態(tài)。因此,讓步的線程基本上是自我調度的??紤]兩個線程在單個CPU上的例子;在這種情況下,我們的基于讓步的方法相當好。如果線程碰巧調用lock()并發(fā)現(xiàn)鎖被持有,它將簡單地讓出CPU,因此另一個線程將運行并完成其臨界區(qū)。
在這個簡單的案例中,讓步方法工作得很好?,F(xiàn)在讓我們考慮有許多線程(比如說100個)反復爭奪鎖的情況。在這種情況下,如果一個線程獲取了鎖并在釋放它之前被搶占,其他99個將各自調用lock(),發(fā)現(xiàn)鎖被持有,并讓出CPU。假設某種輪詢調度器,99個中的每一個都將執(zhí)行這個運行和讓步模式,然后持有鎖的線程再次運行。
雖然比我們的自旋方法更好(這將浪費99個時間片自旋),這種方法仍然成本很高;上下文切換的成本可能相當大,因此有很多浪費。更糟的是,這種方法沒有解決饑餓問題。線程可能會陷入無盡的讓步循環(huán),而其他線程反復進入和退出臨界區(qū)。我們顯然需要一種直接解決饑餓問題的方法。
使用隊列:睡眠而不是自旋
一些先前方法(除了票鎖)的真正問題是,它們留給機會太多。調度器決定了下一個運行的線程;如果調度器做出了錯誤的選擇,運行的線程必須要么自旋等待鎖(我們的第一種方法),要么立即讓出CPU(我們的第二種方法)。無論如何,都有浪費的潛力,也沒有防止饑餓的方法。
因此,我們必須明確控制哪個線程在當前持有者釋放鎖后下一個獲得鎖。為此,我們將需要更多的操作系統(tǒng)支持,以及一個隊列來跟蹤哪些線程正在等待獲取鎖。為簡單起見,我們將使用Solaris提供的支持,以兩個調用的形式:park()讓調用線程睡眠,unpark(threadID)喚醒由threadID指定的特定線程。這兩個例程可以一起使用,構建一個鎖,如果調用者試圖獲取一個被持有的鎖,就讓調用者睡眠,并在鎖空閑時喚醒它。讓我們看看圖28.9中的代碼,以理解這種原始的一種可能用途。
不同的操作系統(tǒng),不同的支持
到目前為止,我們已經(jīng)看到了操作系統(tǒng)可以提供的一種支持,以構建線程庫中更有效的鎖。其他操作系統(tǒng)提供了類似的支持;細節(jié)各不相同。例如,Linux提供了futex,它與Solaris接口類似,但提供了更多的內核功能。具體來說,每個futex都有一個與之相關的特定物理內存位置,以及一個每futex的內核隊列。調用者可以使用futex調用(下面描述)來睡眠和喚醒,如需。具體來說,有兩個調用可用。調用futex wait(address, expected)讓調用線程睡眠,假設address處的值等于expected。如果它不相等,調用立即返回。調用例程futex wake(address)喚醒等待在隊列上的一個線程。
圖28.10(第19頁)中展示了這些調用在Linux互斥鎖中的使用。這個代碼片段來自nptl庫(GNU libc庫的一部分)[L09]中的lowlevellock.h,由于幾個原因很有趣。首先,它使用一個整數(shù)來跟蹤鎖是否被持有(整數(shù)的最高位)以及鎖上的等待者數(shù)量(所有其他位)。因此,如果鎖是負數(shù),它就被持有(因為最高位被設置,而該位決定了整數(shù)的符號)。
其次,代碼片段展示了如何針對常見情況進行優(yōu)化,特別是當沒有鎖爭用時;只有一個線程獲取和釋放鎖時,做的工作很少(原子位測試和設置以鎖定和原子加以釋放鎖)??纯茨隳芊衽宄@個"真實世界"鎖的其余部分是如何工作的。去做吧,成為Linux鎖定的大師,或者至少是當一本書告訴你去做某事時會聽的人3。
兩階段鎖
最后一點:Linux方法有一種舊方法的味道,多年來時而使用,至少可以追溯到1960年代初的Dahm鎖[M82],現(xiàn)在被稱為兩階段鎖。兩階段鎖意識到自旋可能是有用的,特別是如果鎖即將被釋放。因此,在第一階段,鎖自旋一段時間,希望它可以獲取鎖。然而,如果在第一階段自旋期間沒有獲取鎖,則進入第二階段,在此階段,調用者被放入睡眠狀態(tài),只有在鎖稍后變?yōu)榭臻e時才會被喚醒。
上面的Linux鎖是一種這樣的鎖,但它只自旋一次;這種鎖的一般化可以在使用futex支持睡眠之前,在循環(huán)中自旋固定時間。兩階段鎖是混合方法的另一個實例,結合兩個好主意確實可能產(chǎn)生一個更好的主意。當然,是否如此取決于許多事情,包括硬件環(huán)境、線程數(shù)量和其他工作負載細節(jié)。像往常一樣,制造一個適用于所有可能用例的通用鎖是非常具有挑戰(zhàn)性的。
總結
上述方法展示了如今如何構建真正的鎖:一些硬件支持(以更強大的指令形式)加上一些操作系統(tǒng)支持(例如,在Solaris上的park()和unpark()原語,或Linux上的futex)。當然,細節(jié)各不相同,執(zhí)行這種鎖定的確切代碼通常高度優(yōu)化。如果你想了解更多細節(jié),可以查看Solaris或Linux代碼庫;它們是非常有趣的閱讀[L09, S09]。還可以查看David等人的優(yōu)秀工作,比較現(xiàn)代多處理器上不同的鎖定策略[D+13]。
文檔的最后部分提供了一些參考文獻,以及一些關于并發(fā)編程和鎖的實驗性問題,這些問題旨在幫助讀者更好地理解鎖的工作原理和它們在并發(fā)環(huán)境中的行為。
現(xiàn)在不少朋友都在準備校招,常規(guī)的技術學習只是提高了代碼能力,還沒有提升從 0 到 1 整體做項目和解決問題的能力!
為此我們特別推出了C++訓練營,帶著你從 0 到 1 做 C++ 項目(你可以從項目池中任選項目),幫助你提升做項目的能力,提升從0到1的能力,熟悉做項目的完整流程,比如開發(fā)環(huán)境、編譯腳本、架構設計、框架搭建、代碼發(fā)布、問題調試、單元測試。
除了上面的,還有需求分析、項目規(guī)劃、架構設計、任務拆解、時間規(guī)劃、版本管理。另外做項目的過程中必然會遇到種種問題,可以逐步提升你的調試能力,分析問題的能力,掌握更多的調試手段。
遇到棘手的問題,我們還有專門的導師1V1答疑解惑,給出具體建議。
項目池里面的項目,是導師團隊花費大量時間完成的,不僅有完整的代碼及清晰的注釋還有詳細的文檔和視頻,并且有專門的項目導師答疑,完全不用擔心自己學不會。





