互斥鎖,同步鎖,臨界區,互斥量,信號量,自旋鎖之間聯繫是什麼?


互斥鎖和互斥量在我的理解里沒啥區別,不同叫法。廣義上講可以值所有實現互斥作用的同步機制。狹義上講指的就是mutex這種特定的二元鎖機制。

互斥鎖的作用就是互斥,mutual exclusive,是用來保護臨界區(critical section)的。所謂臨界區就是代碼的一個區間,如果兩個線程同時執行就有可能出問題,所以需要互斥鎖來保護。

信號量(semaphore)是一種更高級的同步機制,mutex可以說是semaphore在僅取值0/1時的特例。Semaphore可以有更多的取值空間,用來實現更加複雜的同步,而不單單是線程間互斥。

自旋鎖是一種互斥鎖的實現方式而已,相比一般的互斥鎖會在等待期間放棄cpu,自旋鎖(spinlock)則是不斷循環並測試鎖的狀態,這樣就一直占著cpu。

同步鎖好像沒啥特殊說法,你可以理解為能實現同步作用的都可以叫同步鎖,比如信號量。

最後,不要鑽這些名詞的牛角尖,更重要的是理解這些東西背後的原理,叫什麼名字並沒有什麼好說的。這些東西在不同的語言和平台上又有可能會有不同的叫法,其實本質上就這麼回事。


都在講一件事:【並發】

而且可以再多加幾個詞:競態條件,競態資源,輪詢忙等,鎖變數,原子性,TSL,阻塞,睡眠,喚醒,管程。這樣可以構成一個完整的邏輯鏈條。同意 @Tim Chen 的觀點,不要強記名詞,要理解背後的邏輯。(注!下面講的都專指線程,不是進程)

先寫觀點,這些詞的內在邏輯如下:

  1. 核心矛盾是「競態條件」,即多個線程同時讀寫某個欄位
  2. 競態條件下多線程爭搶的是「競態資源」
  3. 涉及讀寫竟態資源的代碼片段叫「臨界區」
  4. 保證竟態資源安全的最樸素的一個思路就是讓臨界區代碼「互斥」,即同一時刻最多只能有一個線程進入臨界區。
  5. 最樸素的互斥手段:在進入臨界區之前,用if檢查一個bool值,條件不滿足就「忙等」。這叫「鎖變數」
  6. 但鎖變數不是線程安全的。因為「檢查-占鎖」這個動作不具備「原子性」
  7. 「TSL指令」就是原子性地完成「檢查-占鎖」的動作。
  8. 就算不用TSL指令,也可以設計出線程安全的代碼,有一種既巧妙又簡潔的結構叫「自旋鎖」。當然還有其他更複雜的鎖比如「Peterson鎖」。
  9. 但自旋鎖的缺點是條件不滿足時會「忙等待」,需要後台調度器重新分配時間片,效率低。
  10. 解決忙等待問題的是:「sleep」和「wakeup」兩個原語。sleep阻塞當前線程的同時會讓出它佔用的鎖。wakeup可以喚醒在目標鎖上睡眠的線程。
  11. 使用sleep和wakeup原語,保證同一時刻只有一個線程進入臨界區代碼片段的鎖叫「互斥量」
  12. 把互斥鎖推廣到"N"的空間,同時允許有N個線程進入臨界區的鎖叫「信號量」
  13. 互斥量和信號量的實現都依賴TSL指令保證「檢查-占鎖」動作的原子性。
  14. 把互斥量交給程序員使用太危險,有些編程語言實現了「管程」的特性,從編譯器的層面保證了臨界區的互斥,比如Java的synchronized關鍵字。
  15. 並沒有「同步鎖」這個名詞,Java的synchronized正確的叫法應該是「互斥鎖」,「獨佔鎖」或者「內置鎖」。但有的人「顧名思義」叫它同步鎖。

幾個點稍微展開一下,

首先,不是所有變數都可以是競態資源。以Java為例,表示對象狀態的成員欄位可以構成競態資源。方法內部的局部變數就不是競態資源,因為局部變數的生命周期僅局限於方法棧,不能橫跨多個線程。

public class EvenGenerator {
private int even = 0; // 竟態資源

public int nextEven() { // |
++even; // | -&> 臨界區
++even; // |
return even; // |
}
}

上面代碼里的EvenGenerator專門生成偶數。但並發場景下,它會錯誤返回奇數。原因就是當多個線程拿到同一個EvenGenerator對象的引用以後,比如線程A剛執行完第一次自增操作被掛起,線程B接手進行2次自增以後,返回的就是奇數。然後線程A繼續執行,再自增一次以後,也返回奇數。此時even成員欄位構成「競態資源」。訪問竟態資源的代碼nextEven()函數就是臨界區。並發環境中,對象的公有狀態(能通過公有方法訪問也算)暴露給多個線程就構成競態條件,是很危險的。

然後最直觀的保護nextEven()函數的代碼會把調用寫成下面這樣,就是「鎖變數」,

if (!occupied) { // 檢查
occupied = true; // 占鎖
critical_rigion(); // 臨界區
occupied = false; // 釋放鎖
}

但這個做法並沒有卵用。因為A線程完全可能在檢查完occupied鎖變數,確認鎖沒有被佔用以後立刻被掛起。B線程搶佔鎖。這時候再切回A線程,因為已經檢查過鎖變數,它也占鎖,進入臨界區。這時候就同時有兩個線程站在鎖上,互斥失敗。

自旋鎖的關鍵就是用一個while輪詢,代替if檢查狀態,這樣就算線程切出去,另一個線程也因為條件不滿足循環忙等,不會進入臨界區。這是一個非常常用的結構,不光用在自旋鎖,基本是使用條件變數wait(),notifyAll()時候的一種慣用法。

// 線程A
while (true) {
while (turn != 0) {} // 鎖被占,循環忙等。
critical_rigion();
turn = 1; // 釋放鎖
noncritical_rigion();
}
// 線程B
while (true) {
while (turn != 1) {} // 鎖被占,循環忙等
critical_rigion();
turn = 0; // 釋放鎖
noncritical_rigion();
}

但剛才說了自旋鎖的缺點是循環忙等。如果並發的線程不像進程調度那樣在時間片用完以後會自動切換上下文,就會形成死鎖。所以最好在條件不滿足的時候,讓出線程的控制權,讓其他線程有機會執行來使條件滿足。這就是sleep原語做的事情。並且配套的wakeup原語會在條件滿足的情況下喚醒。

結合TSL指令原子性的「檢查-占鎖」,以及sleep阻塞並讓出線程執行權的思想,就是「互斥量」做的事。下面是pthread_mutex_lock的實現(摘自《現代操作系統》)

mutex_lock:
TSL REGISTER,MUTEX |將互斥量複製到寄存器,並且將互斥量重置為1
CMP REGISTER,#0 |互斥量是0嗎?
JZE ok |如果互斥量為0,解鎖,返回
CALL thread_yield |互斥量忙,調度另一線程
JMP mutex_lock |稍後再試
ok: RET |返回調用這,進入臨界區


互斥鎖,同步鎖,臨界區,互斥量,信號量,自旋鎖

一個一個來

互斥鎖:用來互斥的鎖(mutex)

例:TaskA、TaskB 需要讀寫一段共享內存,這種情況就需要使用互斥鎖

main()
{
MutexCreate(mutex, 1);
TaskCreate(TaskA);
TaskCreate(TaskB);
}
void TaskA()
{
MutexTake(mutex);
//讀寫共享內存
MutexGive(mutex);
}
void TaskB()
{
MutexTake(mutex);
//讀寫共享內存
MutexGive(mutex);
}

同步鎖:用來同步的鎖(mutex)

例如:TaskA 作為生產者,需要寫共享內存並通知 TaskB,這種情況使用同步鎖

main()
{
MutexCreate(mutex, 0);
TaskCreate(TaskA);
TaskCreate(TaskB);
}
void TaskA()
{
//寫共享內存

MutexGive(mutex);
}
void TaskB()
{
MutexTake(mutex);
//讀共享內存
}

這兩個都是mutex,只是用在不同場合。

臨界區:凡是訪問了不能多個線程同時訪問的資源的部分代碼。一般就是用互斥鎖鎖起來的代碼。

互斥量、信號量:都是一個意思(Semaphore)。互斥鎖(Mutex)其實可以看成是特殊的信號量,互斥鎖的值最大是1,而信號量的值可以是大於1。

簡單說就是互斥鎖只能鎖一次,信號量可以鎖好幾次。

自旋鎖:等待的時候會佔用CPU的互斥鎖


與其來知乎上問不如去看書啊,《Windows核心編程》裡面就講的很清楚

互斥量就是字面意思,一個互斥量在某個時刻只能被一個線程佔用,如果其他線程想要獲得這個互斥量的使用權就需要等待,然後執行需要保護的代碼,最後釋放互斥量,下一個等待的線程獲得使用權,這樣就可以保證某些內存的原子性訪問

信號量和互斥量本質上一樣,都是用來表示對資源的訪問權,但是互斥量表示資源某個時刻最多只能被一個線程佔用,也就是資源計數最多是1,而信號量的資源計數可以超過1,即同時被多個線程佔用

不管是信號量還是互斥量,線程在等待的時候會從用戶態進入內核態,從而不再佔用cpu時間,當獲得該信號量或互斥量的訪問權時,再由OS調度回用戶態,由於上下文切換以及調度演算法等原因,總的來說開銷是比較大的,對性能會有損耗

自旋鎖也就是字面意思,是通過循環判斷鎖是否被釋放,不會放棄CPU時間進入內核態,因此比較快速,但是因為佔用CPU時間,所以一般只有在發生同步概率很低的時候才會使用自旋鎖

最後我不知道題主說的臨界區是不是跟Windows里的關鍵段類似,我就按照關鍵段說了。關鍵段跟互斥量功能幾乎一樣,都是表示某個資源同一時刻只能被一個線程佔用,區別在於性能,關鍵段的的性能要優於互斥量,它的原理是先自旋一段時間,如果等不到再進入內核態


互斥鎖:用於保護臨界區,確保同一時間只有一個線程訪問數據。對共享資源的訪問,先對互斥量進行加鎖,如果互斥量已經上鎖,調用線程會阻塞,直到互斥量被解鎖。在完成了對共享資源的訪問後,要對互斥量進行解鎖。

臨界區:每個進程中訪問臨界資源的那段程序稱為臨界區,每次只允許一個進程進入臨界區,進入後不允許其他進程進入。

自旋鎖:與互斥量類似,它不是通過休眠使進程阻塞,而是在獲取鎖之前一直處於忙等(自旋)阻塞狀態。用在以下情況:鎖持有的時間短,而且線程並不希望在重新調度上花太多的成本。"原地打轉"。

自旋鎖與互斥鎖的區別:線程在申請自旋鎖的時候,線程不會被掛起,而是處於忙等的狀態。

信號量:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。


互斥鎖針對的是共享資源,共享資源在某種情況下需要只有一個線程訪問,為了達到這種機制,使用互斥鎖來達到這種目的。臨界區是互斥的另一種表現,是由程序員自己規定的代碼範圍(操作共享資源的代碼),以滿足多線程下的邏輯要求。

同步鎖主要目的在於協調線程執行的的先後順序(形成線程執行序列),個人感覺同步鎖是互斥鎖的一個特例,主要功能用於協調線程,但是其中也涉及到線程共享的資源。


所有這些就是為了實現同步和互斥,互斥不用講,很簡單,同步稍難,不過一個詞概括就是時序!


建議看看《深入理解linux內核》


不同進程或相同進程不同線程之間對資源的使用與通信所產生的問題及其解決方案即是樓主所說的這些單詞了,不同大方案里的稱呼罷了


推薦閱讀:

C# 如何在調用控制項時做到 Thread-safe(線程安全)?
多線程是否有意義?
boost 是否像 Linux 一樣提供讀寫自旋鎖機制?
多線程讀內存變慢如何解決?

TAG:操作系統 | 多線程 | 多線程編程 | 並發 | 自旋鎖編程 |