golang mutex 一旋二餓三喚醒機制

偉大的事業,需要決心,能力,組織和責任感。 ——易卜生

1 前言

接着上篇 mutex 文章,我們繼續探討 mutex 的其他特性,比如自旋,飢餓模式以及喚醒狀態等。

2 自旋

2.1 自旋的過程

加鎖時,如果當前 Locked 位爲 1,說明該鎖當前由其他協程持有,嘗試加鎖的協程並不是馬上轉入阻塞,而是會持續的探測 Locked 位是否變爲 0,這個過程即爲自旋過程。

自旋時間很短,但如果在自旋過程中發現鎖已被釋放,那麼協程可以立即獲取鎖。此時即便有協程被喚醒也無法獲取鎖,只能再次阻塞。

自旋的好處是,當加鎖失敗時不必立即轉入阻塞,有一定機會獲取到鎖,這樣可以避免協程的切換。

2.2 自旋的定義

自旋對應的是 CPU 的”PAUSE” 指令,CPU 對該指令什麼都不做,相當於 CPU 空轉,對程序而言相當於 sleep 了一小段時間,時間非常短,當前實現是 30 個時鐘週期。

自旋過程中會持續探測 Locked 是否變爲 0,連續兩次探測間隔就是執行這些 PAUSE 指令,它不同於 sleep,不需要將協程轉爲睡眠狀態

2.3 自旋的條件

加鎖時程序會自動判斷是否可以自旋,無限制的自旋將會給 CPU 帶來巨大壓力,所以判斷是否可以自旋就很重要了。

自旋必須滿足以下所有條件:

可見,自旋的條件是很苛刻的,總而言之就是不忙的時候纔會啓用自旋。

2.4 自旋的優勢

自旋的優勢是更充分的利用 CPU,儘量避免協程切換。因爲當前申請加鎖的協程擁有 CPU,如果經過短時間的自旋可以獲得鎖,當前協程可以繼續運行,不必進入阻塞狀態。

2.5 自旋的問題

如果自旋過程中獲得鎖,那麼之前被阻塞的協程將無法獲得鎖,如果加鎖的協程特別多,每次都通過自旋獲得鎖,那麼之前被阻塞的進程將很難獲得鎖,從而進入飢餓狀態。

爲了避免協程長時間無法獲取鎖,自 1.8 版本以來增加了一個狀態,即 Mutex 的 Starving 狀態。這個狀態下不會自旋,一旦有協程釋放鎖,那麼一定會喚醒一個協程併成功加鎖。

3 mutex 模式

3.1 正常模式

默認情況下,Mutex 的模式爲 normal。

該模式下,協程如果加鎖不成功不會立即轉入阻塞排隊,而是判斷是否滿足自旋的條件,如果滿足則會啓動自旋過程,嘗試搶鎖。

3.2 飢餓模式

自旋過程中能搶到鎖,一定意味着同一時刻有協程釋放了鎖,我們知道釋放鎖時如果發現有阻塞等待的協程,還會釋放一個信號量來喚醒一個等待協程,被喚醒的協程得到 CPU 後開始運行,此時發現鎖已被搶佔了,自己只好再次阻塞,不過阻塞前會判斷自上次阻塞到本次阻塞經過了多長時間,如果超過 1ms 的話,會將 Mutex 標記爲” 飢餓” 模式,然後再阻塞。

處於飢餓模式下,不會啓動自旋過程,也即一旦有協程釋放了鎖,那麼一定會喚醒協程,被喚醒的協程將會成功獲取鎖,同時也會把等待計數減 1。

3.3 喚醒模式

Woken 狀態用於加鎖和解鎖過程的通信,舉個例子,同一時刻,兩個協程一個在加鎖,一個在解鎖,在加鎖的協程可能在自旋過程中,此時把 Woken 標記爲 1,用於通知解鎖協程不必釋放信號量了,好比在說:你只管解鎖好了,不必釋放信號量,我馬上就拿到鎖了。

4 tips

4.1 重複 unlock 會 panic

可能你會想,爲什麼 Go 不能實現得更健壯些,多次執行 Unlock() 也不要 panic?

仔細想想 Unlock 的邏輯就可以理解,這實際上很難做到。Unlock 過程分爲將 Locked 置爲 0,然後判斷 Waiter 值,如果值 > 0,則釋放信號量。

如果多次 Unlock(),那麼可能每次都釋放一個信號量,這樣會喚醒多個協程,多個協程喚醒後會繼續在 Lock() 的邏輯裏搶鎖,勢必會增加 Lock() 實現的複雜度,也會引起不必要的協程切換。

4.2 使用 defer 避免死鎖

加鎖後立即使用 defer 對其解鎖,可以有效的避免死鎖。

4.3 加鎖和解鎖應該成對出現

加鎖和解鎖最好出現在同一個層次的代碼塊中,比如同一個函數。

5 小結

只有徹底掌握了 mutex 的原理,才能在代碼中游刃有餘,否則寫出來的併發控制代碼可能隨時在面臨高併發的場景中出現崩潰的局面。

6 關注公衆號

微信公衆號:堆棧 future

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vIbz4_yPaeh9Uoj1qeknWQ