互斥鎖是併發程式中對共享資源進行訪問控制的主要手段,對此go語言提供了非常簡單易用的mutex,mutex為一結構體型別,對外暴露兩個方法lock()和unlock()分別用於加鎖和解鎖。
mutex使用起來非常方便,但其內部實現卻複雜得多,這包括mutex的幾種狀態。另外,我們也想**一下mutex重複解鎖引起panic的原因。
按照慣例,本節內容從原始碼入手,提取出實現原理,又不會過分糾結於實現細節。
原始碼包src/sync/mutex.go:mutex定義了互斥鎖的資料結構:
type mutex struct
協程之間搶鎖實際上是搶給locked賦值的權利,能給locked域置1,就說明搶鎖成功。搶不到的話就阻塞等待mutex.sema訊號量,一旦持有鎖的協程解鎖,等待的協程會依次被喚醒。
woken和starving主要用於控制協程間的搶鎖過程,後面再進行了解。
mutext對外提供兩個方法,實際上也只有這兩個方法:
下面我們分析一下加鎖和解鎖的過程,加鎖分成功和失敗兩種情況,成功的話直接獲取鎖,失敗後當前協程被阻塞,同樣,解鎖時跟據是否有阻塞協程也有兩種處理。
假定當前只有乙個協程在加鎖,沒有其他協程干擾,那麼過程如下圖所示:
加鎖過程會去判斷locked標誌位是否為0,如果是0則把locked位置1,代表加鎖成功。從上圖可見,加鎖成功後,只是locked位置1,其他狀態位沒發生變化。
假定加鎖時,鎖已被其他協程占用了,此時加鎖過程如下圖所示:
從上圖可看到,當協程b對乙個已被占用的鎖再次加鎖時,waiter計數器增加了1,此時協程b將被阻塞,直到locked值變為0後才會被喚醒。
假定解鎖時,沒有其他協程阻塞,此時解鎖過程如下圖所示:
由於沒有其他協程阻塞等待加鎖,所以此時解鎖時只需要把locked位置為0即可,不需要釋放訊號量。
假定解鎖時,有1個或多個協程阻塞,此時解鎖過程如下圖所示:
協程a解鎖過程分為兩個步驟,一是把locked位置0,二是檢視到waiter>0,所以釋放乙個訊號量,喚醒乙個阻塞的協程,被喚醒的協程b把locked位置1,於是協程b獲得鎖。
加鎖時,如果當前locked位為1,說明該鎖當前由其他協程持有,嘗試加鎖的協程並不是馬上轉入阻塞,而是會持續的探測locked位是否變為0,這個過程即為自旋過程。
自旋時間很短,但如果在自旋過程中發現鎖已被釋放,那麼協程可以立即獲取鎖。此時即便有協程被喚醒也無法獲取鎖,只能再次阻塞。
自旋的好處是,當加鎖失敗時不必立即轉入阻塞,有一定機會獲取到鎖,這樣可以避免協程的切換。
自旋對應於cpu的"pause"指令,cpu對該指令什麼都不做,相當於cpu空轉,對程式而言相當於sleep了一小段時間,時間非常短,當前實現是30個時鐘週期。
自旋過程中會持續探測locked是否變為0,連續兩次探測間隔就是執行這些pause指令,它不同於sleep,不需要將協程轉為睡眠狀態。
加鎖時程式會自動判斷是否可以自旋,無限制的自旋將會給cpu帶來巨大壓力,所以判斷是否可以自旋就很重要了。
自旋必須滿足以下所有條件:
自旋的優勢是更充分的利用cpu,盡量避免協程切換。因為當前申**鎖的協程擁有cpu,如果經過短時間的自旋可以獲得鎖,當前協程可以繼續執行,不必進入阻塞狀態。
如果自旋過程中獲得鎖,那麼之前被阻塞的協程將無法獲得鎖,如果加鎖的協程特別多,每次都通過自旋獲得鎖,那麼之前被阻塞的程序將很難獲得鎖,從而進入飢餓狀態。
為了避免協程長時間無法獲取鎖,自1.8版本以來增加了乙個狀態,即mutex的starving狀態。這個狀態下不會自旋,一旦有協程釋放鎖,那麼一定會喚醒乙個協程並成功加鎖。
前面分析加鎖和解鎖過程中只關注了waiter和locked位的變化,現在我們看一下starving位的作用。
每個mutex都有兩個模式,稱為normal和starving。下面分別說明這兩個模式。
預設情況下,mutex的模式為normal。
該模式下,協程如果加鎖不成功不會立即轉入阻塞排隊,而是判斷是否滿足自旋的條件,如果滿足則會啟動自旋過程,嘗試搶鎖。
自旋過程中能搶到鎖,一定意味著同一時刻有協程釋放了鎖,我們知道釋放鎖時如果發現有阻塞等待的協程,還會釋放乙個訊號量來喚醒乙個等待協程,被喚醒的協程得到cpu後開始執行,此時發現鎖已被搶占了,自己只好再次阻塞,不過阻塞前會判斷自上次阻塞到本次阻塞經過了多長時間,如果超過1ms的話,會將mutex標記為"飢餓"模式,然後再阻塞。
處於飢餓模式下,不會啟動自旋過程,也即一旦有協程釋放了鎖,那麼一定會喚醒協程,被喚醒的協程將會成功獲取鎖,同時也會把等待計數減1。
woken狀態用於加鎖和解鎖過程的通訊,舉個例子,同一時刻,兩個協程乙個在加鎖,乙個在解鎖,在加鎖的協程可能在自旋過程中,此時把woken標記為1,用於通知解鎖協程不必釋放訊號量了,好比在說:你只管解鎖好了,不必釋放訊號量,我馬上就拿到鎖了。
可能你會想,為什麼go不能實現得更健壯些,多次執行unlock()也不要panic?
仔細想想unlock的邏輯就可以理解,這實際上很難做到。unlock過程分為將locked置為0,然後判斷waiter值,如果值》0,則釋放訊號量。
如果多次unlock(),那麼可能每次都釋放乙個訊號量,這樣會喚醒多個協程,多個協程喚醒後會繼續在lock()的邏輯裡搶鎖,勢必會增加lock()實現的複雜度,也會引起不必要的協程切換。
加鎖後立即使用defer對其解鎖,可以有效的避免死鎖。
加鎖和解鎖最好出現在同乙個層次的**塊中,比如同乙個函式。
重複解鎖會引起panic,應避免這種操作的可能性。
內容源自:
go 互斥鎖實現原理
目錄2.加解鎖過程 3.自旋過程 4.mutex模式 5.woken狀態 6.為什麼重複解鎖要panic go中通過mutex來實現對互斥資源的鎖定 go type mutex struce下圖展示了mutex的記憶體布局 協程之間搶鎖的過程實際上是給locked賦值1的過程,能給locked賦值為...
Go 互斥鎖和讀寫互斥鎖的實現
目錄 先來看這樣一段 所存在的問題 var wg sync.waitgroup var x int64 func main func f wg.done 這裡為什麼輸出是 12135 不同的機器結果不一樣 而不是20000。因為 x 的賦值,總共分為三個步驟 取出x的值 計算x的結果 給x賦值。那麼...
執行緒同步與互斥 實現互斥鎖
今天我們來分享一下,執行緒同步與互斥 互斥鎖的實現。多個執行緒同時訪問共享資料時可能會產生衝突,造成程式執行結果不是我們所預期的結果。不產生衝突的多執行緒訪問情況,和截圖如下 產生衝突的多執行緒訪問情況,和截圖如下 注 每執行一次,結果都可能會不同。由於多執行緒訪問共享資料時可能會產生衝突,不能保證...