1. 程式人生 > 其它 >【Linux系統程式設計】互斥鎖和自旋鎖

【Linux系統程式設計】互斥鎖和自旋鎖

簡介

多執行緒訪問共享資源的時候,避免不了資源競爭而導致資料錯亂的問題,所以我們通常為了解決這一問題,都會在訪問共享資源之前加鎖。

 互斥鎖與自旋鎖

最底層的兩種就是會「互斥鎖和自旋鎖」,有很多高階的鎖都是基於它們實現的,你可以認為它們是各種鎖的地基,所以我們必須清楚它倆之間的區別和應用。

加鎖的目的就是保證共享資源在任意時間裡,只有一個執行緒訪問,這樣就可以避免多執行緒導致共享資料錯亂的問題。

當已經有一個執行緒加鎖後,其他執行緒加鎖則就會失敗,互斥鎖和自旋鎖對於加鎖失敗後的處理方式是不一樣的:

  • 互斥鎖加鎖失敗後,執行緒會釋放 CPU ,給其他執行緒;
  • 自旋鎖加鎖失敗後,執行緒會忙等待,直到它拿到鎖;

互斥鎖是一種「獨佔鎖」,比如當執行緒 A 加鎖成功後,此時互斥鎖已經被執行緒 A 獨佔了,只要執行緒 A 沒有釋放手中的鎖,執行緒 B 加鎖就會失敗,於是就會釋放 CPU 讓給其他執行緒,既然執行緒 B 釋放掉了 CPU,自然執行緒 B 加鎖的程式碼就會被阻塞。

對於互斥鎖加鎖失敗而阻塞的現象,是由作業系統核心實現的。當加鎖失敗時,核心會將執行緒置為「睡眠」狀態,等到鎖被釋放後,核心會在合適的時機喚醒執行緒,當這個執行緒成功獲取到鎖後,於是就可以繼續執行。如下圖:

 

所以,互斥鎖加鎖失敗時,會從使用者態陷入到核心態,讓核心幫我們切換執行緒,雖然簡化了使用鎖的難度,但是存在一定的效能開銷成本。

那這個開銷成本是什麼呢?會有兩次執行緒上下文切換的成本:

  • 當執行緒加鎖失敗時,核心會把執行緒的狀態從「執行」狀態設定為「睡眠」狀態,然後把 CPU 切換給其他執行緒執行;
  • 接著,當鎖被釋放時,之前「睡眠」狀態的執行緒會變為「就緒」狀態,然後核心會在合適的時間,把 CPU 切換給該執行緒執行。

執行緒的上下文切換的是什麼?當兩個執行緒是屬於同一個程序,因為虛擬記憶體是共享的,所以在切換時,虛擬記憶體這些資源就保持不動,只需要切換執行緒的私有資料、暫存器等不共享的資料。

上下切換的耗時有大佬統計過,大概在幾十納秒到幾微秒之間,如果你鎖住的程式碼執行時間比較短,那可能上下文切換的時間都比你鎖住的程式碼執行時間還要長。

所以,如果你能確定被鎖住的程式碼執行時間很短,就不應該用互斥鎖,而應該選用自旋鎖,否則使用互斥鎖。

自旋鎖是通過 CPU 提供的 CAS 函式(Compare And Swap),在「使用者態」完成加鎖和解鎖操作,不會主動產生執行緒上下文切換,所以相比互斥鎖來說,會快一些,開銷也小一些。

一般加鎖的過程,包含兩個步驟:

  • 第一步,檢視鎖的狀態,如果鎖是空閒的,則執行第二步;
  • 第二步,將鎖設定為當前執行緒持有;

CAS 函式就把這兩個步驟合併成一條硬體級指令,形成原子指令,這樣就保證了這兩個步驟是不可分割的,要麼一次性執行完兩個步驟,要麼兩個步驟都不執行。

使用自旋鎖的時候,當發生多執行緒競爭鎖的情況,加鎖失敗的執行緒會「忙等待」,直到它拿到鎖。這裡的「忙等待」可以用 while 迴圈等待實現,不過最好是使用 CPU 提供的 PAUSE 指令來實現「忙等待」,因為可以減少迴圈等待時的耗電量。

自旋鎖是最比較簡單的一種鎖,一直自旋,利用 CPU 週期,直到鎖可用。需要注意,在單核 CPU 上,需要搶佔式的排程器(即不斷通過時鐘中斷一個執行緒,執行其他執行緒)。否則,自旋鎖在單 CPU 上無法使用,因為一個自旋的執行緒永遠不會放棄 CPU。

自旋鎖開銷少,在多核系統下一般不會主動產生執行緒切換,適合非同步、協程等在使用者態切換請求的程式設計方式,但如果被鎖住的程式碼執行時間過長,自旋的執行緒會長時間佔用 CPU 資源,所以自旋的時間和被鎖住的程式碼執行的時間是成「正比」的關係,我們需要清楚的知道這一點。

自旋鎖與互斥鎖使用層面比較相似,但實現層面上完全不同:當加鎖失敗時,互斥鎖用「執行緒切換」來應對,自旋鎖則用「忙等待」來應對。

它倆是鎖的最基本處理方式,更高階的鎖都會選擇其中一個來實現,比如讀寫鎖既可以選擇互斥鎖實現,也可以基於自旋鎖實現。