互斥鎖 vs 自旋鎖

2022-06-14 08:15:12 字數 4214 閱讀 8746

本文首發於:行者ai

鎖在生活中用處很直接,比如給電瓶車加鎖就是防止被偷。在程式設計世界裡,「鎖」就五花八門了,它們有著各自不同的開銷和應用場景。在存在資料競爭的場景,如果選對了鎖,能大大提高系統效能,否則會互相拖後腿,效能急劇降低。

加鎖的目的就是保證共享資源在任意時間內,只有乙個執行緒可以訪問,以此避免資料共享導致錯亂的問題。最底層就是兩種鎖:「互斥鎖」和「自旋鎖」,其他高階鎖,如讀寫鎖、悲觀鎖、樂觀鎖等都是基於它們實現的。

想知道它們誰更高效,要先了解它們在做同一件事情的行為有何不同。假設有乙個執行緒加鎖成功,其他執行緒加鎖自然會失敗,失敗執行緒的處理方式如下:

持有互斥鎖的執行緒在看到鎖已經有主了之後,就會禮貌的退出,等待之後鎖釋放時自己被系統喚醒;而自旋鎖呢,它居然在反覆的詢問鎖使用完了沒有,這實在是... 我寫個while迴圈反覆爭奪資源,那不就是自旋鎖咯?不會吧,不會吧,不會真的有人用自旋鎖吧?誰更輕鬆高效這不是一目了然嗎?

其實吧,自旋鎖也沒那麼不堪,使用場景還挺多,在很多場合比互斥鎖更好用,我要在本文給自旋鎖洗地。至於怎麼洗,那需要詳細說說它們各自的原理,工程方面的選擇,還真就是這麼神奇。

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

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

互斥鎖加鎖失敗,就會從使用者態陷入核心態,核心幫我們切換執行緒,這簡化了互斥鎖使用的難度,但也存在效能開銷。

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

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

上下文切換需要幾十納秒到幾微秒之間,如果鎖住的**執行時間極短(常見情況),那花在兩次上下文切換的時間就會遠多於鎖住**的執行時長。而且,執行緒的私有資料已經在cpu的cache上都預熱好了,這一出一進,資料可能就涼透了,之後反覆的cache miss那可就真的酸爽。所以,鎖住的**執行只需要幾納秒的話,為啥不持有cpu繼續自旋等待呢?

上面的互斥鎖都基於乙個假設: 這鎖小明拿了,其他人都不可能再染指,除非小明不要了。咦! 這是咋做到的?

先考慮單核場景:能不能硬體做一種加鎖的原子操作呢?能! 「test and set」指令就是做這個事情的,因為自己是一條硬體指令,最小執行單位,絕對不可能被打斷。有了」test and set"原子指令,單核環境下,鎖的實現問題得到了圓滿的解決。

那麼多核環境呢?簡單嘛,還是「test and set」不就得了,這是一條指令,原子的,不會有問題的。真的嗎?單獨一條指令能夠保證該指令在單個核上執行過程中不會被中斷,但是兩個核同時執行這個指令呢?再想想,硬體執行時還是得從記憶體中讀取lock,判斷並設定狀態到記憶體,貌似這個過程也不是那麼原子嘛,這可真是套娃啊。那多個核執行怎麼辦呢?首先我們得明白這個地方的關鍵點,關鍵點是兩個核會並行操作記憶體而且從操作記憶體這個排程來看「test and set」不是原子的,需要先讀記憶體然後再寫記憶體,如果我們保證這個記憶體操作不能並行,那就回歸單核場景了呀!剛好,硬體提供了鎖記憶體匯流排的機制,我們在鎖記憶體匯流排的狀態下執行test and set操作,就能保證同時只有乙個核來test and set,從而避免了多核下發生的問題。

在x86 平台上,cpu提供了在指令執行期間對匯流排加鎖 的手段。cpu晶元上有一條引線#hlock pin,如果組合語言的程式中在一條指令前面加上字首"lock" ,經過彙編以後的機器**就使cpu在執行這條指令的時候把#hlock pin的電位拉低,持續到這條指令結束時放開,從而把匯流排鎖住,這樣同一匯流排上別的cpu就暫時不能通過匯流排訪問記憶體了,保證了這條指令在多處理器環境中的原子性。

能夠和 lock 指令字首一起使用的指令如下所示:

bt, bts, btr, btc (mem, reg/imm)

xchg, xadd (reg, mem / mem, reg)

add, or, adc, sbb (mem, reg/imm)

and, sub, xor (mem, reg/imm)

not, neg, inc, dec (mem)

自旋鎖是最比較簡單的一種鎖,一直自旋,利用 cpu 週期,直到鎖可用。需要注意,在單核 cpu 上,需要搶占式的排程器(即通過時鐘中斷乙個執行緒,執行其他執行緒)。否則,自旋鎖在單 cpu 上無法使用,因為乙個自旋的執行緒永遠不會放棄 cpu。自旋鎖開銷少,在多核系統下一般不會主動產生執行緒切換,適合非同步、協程等在使用者態切換請求的程式設計方式,但如果被鎖住的**執行時間過長,自旋的執行緒會長時間占用 cpu 資源,所以自旋的時間和被鎖住的**執行的時間是成「正比」的關係,我們需要清楚的知道這一點。

自旋鎖與互斥鎖使用層面比較相似,但實現層面上完全不同:當加鎖失敗時,互斥鎖用「執行緒切換」來應對,自旋鎖則用「忙等待」來應對。這裡的忙等待,可以用「while」迴圈實現,但最好不要這麼幹!!cpu提供了「pause」指令來實現忙等待。

自旋鎖不就是不停的while迴圈去獲取鎖,還需要講原理?等等,去獲取鎖狀態的時候怎麼保證資料原子性?難道又用互斥鎖?如果真套一層互斥鎖,那我就給自旋鎖洗不了地了。顯然在這裡不能這麼套娃!

反覆嘗試加鎖的時候,包含兩個步驟:

這個過程叫做「compare and swap」,簡稱「cas」,它把上述兩個步驟合併成一條硬體級指令,在「使用者態」完成加鎖和解鎖操作,不會主動產生執行緒上下文切換,所以相比互斥鎖來說,會快一些,開銷也小一些。

上面說,不推薦while迴圈獲取鎖,intel cpu提供的「pause」指令,「pause」指令是什麼?那它如何解決無腦while迴圈占用cpu且低效率的問題呢?

其實自旋鎖不會主動釋放cpu,所以不可能解決占用cpu的問題,但能讓這個過程更省電,搶占鎖效率更高。

「pause」指令通過讓cpu休息一定的時鐘週期,在此休息期間,耗電幾乎停滯。休息的時鐘週期,不同版本cpu不一樣,大概在幾十到上百時鐘週期之間。以5ghz主頻執行的cpu為例,乙個時鐘週期就是0.2納秒。

休息的時鐘週期不是越大越好。比如intel新一代的skylake架構中,初期「pause」指令的休息週期高達140個時鐘週期。這直接導致mysql在理論上效能更好的cpu上,資料庫效能跑出了比前幾年cpu更糟糕的成績,擠出的牙膏吸回去了!在隨後的步進中降低了「pause」的時鐘週期到上一代的10個時鐘週期,資料庫展現的效能才恢復了牙膏廠該有的水準(每代效能提公升一丟丟)。

另乙個優點跟流水線有關係,頻繁的檢測會讓流水線上充滿了讀操作。另外乙個執行緒往流水線上丟入乙個鎖變數寫操作的時候,必須對流水線進行重排,因為cpu必須保證所有讀操作讀到正確的值。流水線重排十分耗時,影響lock()的效能。設想一下,當乙個獲得鎖的工作執行緒w從臨界區退出,在呼叫unlock釋放鎖的時候,有若干個等待執行緒s都在自旋檢測鎖是否可用,此時w執行緒會產生乙個store指令,若干個s執行緒會產生很多load指令,在store之後的load指令要等待store在流水線上執行完畢才能執行,由於處理器是亂序執行,在沒有store指令之前,處理器對多個沒有依賴的load是可以隨機亂序執行的,當有了store指令之後,需要reorder重新排序執行,此時會嚴重影響處理器效能,按照intel的說法,會帶來25倍的效能損失。pause指令的作用就是減少並行load的數量,從而減少reorder時所耗時間。

互斥鎖和自旋鎖沒有優略之分,工程中使用哪種鎖,主要還是看使用場景(洗地操作)。

一般情況使用互斥鎖。如果我們明確知道被鎖住的**的執行時間很短(這樣的場景最普遍,就算不普遍也要改**讓這種場景普遍),那我們應該選擇開銷比較小的自旋鎖,因為自旋鎖加鎖失敗時,並不會主動產生執行緒切換,而是一直忙等待,直到獲取到鎖,那麼如果被鎖住的**執行時間很短,那這個忙等待的時間相對應也很短。

不管使用的哪種鎖,我們的加鎖的**範圍應該盡可能的小,也就是加鎖的粒度要小,這樣執行速度會比較快。

自旋鎖和互斥鎖

1.理論分析 從理論上說,如果乙個執行緒嘗試加鎖乙個互斥鎖的時候沒有成功,因為互斥鎖已經被鎖住了,這個未獲取鎖的執行緒會休眠以使得其它執行緒可以馬上執行。這個執行緒會一直休眠,直到持有鎖的執行緒釋放了互斥鎖,休眠的執行緒才會被喚醒。如果乙個執行緒嘗試獲得乙個自旋鎖的時候沒有成功,該執行緒會一直嘗試加...

互斥鎖與自旋鎖

一 互斥鎖 當鎖時可用的,呼叫上鎖的api會成功,並且將鎖設定為不再可用。當乙個程序嘗試獲取不可用的鎖的時候它會阻塞,直到鎖被釋放。進入臨界區時獲得鎖,退出臨界區時釋放鎖。二 自旋鎖 是指當乙個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲...

自旋鎖與互斥鎖

互斥鎖,就是悲觀鎖,保證乙個執行緒進去。執行緒會從sleep 加鎖 runng 解鎖 過程中有上下文的切換,cpu的搶占,訊號的傳送等開銷。自旋鎖 執行緒一直都是running 加鎖 解鎖 死迴圈檢測鎖位的標誌位,機制不複雜。自旋鎖 由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋鎖而不是睡眠是非...