Go運行時中的 Mutex
我在極客時間上開了一門面向中高級Go程序員的課程:Go 并發編程實戰課,有讀者問Go channel中的實現中使用了mutex,這個mutex和標準庫中的Mutex有什么不同?正好在百度廠內分享Go相關課程中有同事也提出了相同的問題,所以我專門寫一篇文章介紹一下。
sync.Mutex是一個high level的同步原語,是為廣大的Go開發者開發應用程序提供的一種數據結構,現在它的內部實現邏輯比較復雜了,包含spin和饑餓處理等邏輯,它底層使用了運行時的low level的一些函數和atomic的一些方法。
而運行時中的mutex是為運行時內部使用互斥鎖而提供的一個同步原語,它提供了spin和等待隊列,并沒有去解決饑餓狀態,而且它的實現和sync.Mutex的實現也是不一樣的。它并沒有以方法的方式提供Lock/Unlock,而是提供lock/unlock函數實現請求鎖和釋放鎖。
Dan Scales 今年年初的時候又為運行時的鎖增加了static locking rank的功能。他為運行時的架構無關的鎖( architecture-independent locks)定義了rank,并且又定義了一些運行時的鎖的偏序(此鎖之前允許持有哪些鎖)。這是運行時鎖的一個巨大改變,但是很遺憾并沒有一篇設計文檔詳細去描述這個功能的設計,你可以通過提交的comment(#0a820007)和代碼中的注釋去了解runtime內部鎖的代碼變化。
本質上來說,這個功能用來檢查鎖的順序是不是按照文檔設計的順序執行的,如果有違反設定的順序,就有可能死鎖發生。因為缺乏準確的文檔說明,并且這個功能主要是用來檢查運行時鎖的執行順序的,所以在本文中我把這一段邏輯抹去不介紹了。實際Go運行時要開始這個檢查的話,你需要設置變量GOEXPERIMENT=staticlockranking。
那么接下來我們看看運行時的mutex的數據結構的定義以及lock/unlock的實現。
運行時mutex數據結構
運行時的mutex數據結構很簡單,如下所示,定義在runtime2.go中:
- type mutex struct {
- lockRankStruct
- // Futex-based impl treats it as uint32 key,
- // while sema-based impl as M* waitm.
- // Used to be a union, but unions break precise GC.
- key uintptr
- }
如果不啟用lock ranking,其實lockRankStruct就是一個空結構:
- type lockRankStruct struct {
- }
那么對于運行時的mutex,最重要的就是key字段了。這個字段針對不同的架構有不同的含義。
對于dragonfly、freebsd、linux架構,mutex會使用基于Futex的實現, key就是一個uint32的值。 Linux提供的Futex(Fast user-space mutexes)用來構建用戶空間的鎖和信號量。Go 運行時封裝了兩個方法,用來sleep和喚醒當前線程:
- futexsleep(addr uint32, val uint32, ns int64):原子操作`if addr == val { sleep }`。
- futexwakeup(addr *uint32, cnt uint32):喚醒地址addr上的線程最多cnt次。
對于其他的架構,比如aix、darwin、netbsd、openbsd、plan9、solaris、windows,mutex會使用基于sema的實現,key就是M* waitm。Go 運行時封裝了三個方法,用來創建信號量和sleep/wakeup:
- func semacreate(mp *m):創建信號量
- func semasleep(ns int64) int32: 請求信號量,請求不到會休眠一段時間
- func semawakeup(mp *m):喚醒mp
基于這兩種實現,分別有不同的lock和unlock方法的實現,主要邏輯都是類似的,所以接下來我們只看基于Futex的lock/unlock。
請求鎖lock
如果不使用lock ranking特性,lock的邏輯主要是由lock2實現的。
- func lock(l *mutex) {
- lockWithRank(l, getLockRank(l))
- }
- func lockWithRank(l *mutex, rank lockRank) {
- lock2(l)
- }
- func lock2(l *mutex) {
- // 得到g對象
- gp := getg()
- // g綁定的m對象的lock計數加1
- if gp.m.locks < 0 {
- throw("runtime·lock: lock count")
- }
- gp.m.locks++
- // 如果有幸運光環,原來鎖沒有被持有,一把就獲取到了鎖,就快速返回了
- v := atomic.Xchg(key32(&l.key), mutex_locked)
- if v == mutex_unlocked {
- return
- }
- // 否則原來的可能是MUTEX_LOCKED或者MUTEX_SLEEPING
- wait := v
- // 單核不進行spin,多核CPU情況下會嘗試spin
- spin := 0
- if ncpu > 1 {
- spin = active_spin
- }
- for {
- // 嘗試spin,如果鎖已經釋放,嘗試搶鎖
- for i := 0; i < spin; i++ {
- for l.key == mutex_unlocked {
- if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
- return
- }
- }
- // PAUSE
- procyield(active_spin_cnt)
- }
- // 再嘗試搶鎖, rescheduling.
- for i := 0; i < passive_spin; i++ {
- for l.key == mutex_unlocked {
- if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
- return
- }
- }
- osyield()
- }
- // 再嘗試搶鎖,并把key設置為mutex_sleeping,如果搶鎖成功,返回
- v = atomic.Xchg(key32(&l.key), mutex_sleeping)
- if v == mutex_unlocked {
- return
- }
- // 否則sleep等待
- wait = mutex_sleeping
- futexsleep(key32(&l.key), mutex_sleeping, -1)
- }
- }
unlock
如果不使用lock ranking特性,unlock的邏輯主要是由unlock2實現的。
- func unlock(l *mutex) {
- unlockWithRank(l)
- }
- func unlockWithRank(l *mutex) {
- unlock2(l)
- }
- func unlock2(l *mutex) {
- // 將key的值設置為mutex_unlocked
- v := atomic.Xchg(key32(&l.key), mutex_unlocked)
- if v == mutex_unlocked {
- throw("unlock of unlocked lock")
- }
- // 如果原來有線程在sleep,喚醒它
- if v == mutex_sleeping {
- futexwakeup(key32(&l.key), 1)
- }
- //得到當前的goroutine以及和它關聯的m,將鎖的計數減1
- gp := getg()
- gp.m.locks--
- if gp.m.locks < 0 {
- throw("runtime·unlock: lock count")
- }
- if gp.m.locks == 0 && gp.preempt { // restore the preemption request in case we've cleared it in newstack
- gp.stackguard0 = stackPreempt
- }
- }
總體來說,運行時的mutex邏輯還不太復雜,主要是需要處理不同的架構的實現,它休眠喚醒的對象是m,而sync.Mutex休眠喚醒的對象是g。