go語言之Mutex

mutex工作機制

Mutex有兩種工作模式:正常模式和飢餓模式
在正常模式中,等待着按照FIFO的順序排隊獲取鎖,但是一個被喚醒的等待者有時候並不能獲取mutex,它還需要和新到來的goroutine們競爭mutex的使用權。新到來的goroutine有一個優勢,因爲新到達的goroutine已經在CPU上運行了,因此一個被喚醒的等待者有很大的概率獲取不到鎖。在這種情況下它處在等待隊列的前面。如果一個goroutine等待mutex釋放的時間超過1ms,它就會將mutex切換到飢餓模式。

在飢餓模式中,mutex的所有權直接從解鎖的goroutine遞交到等待隊列中排在最前方的goroutine。新到達的goroutine們不要嘗試去獲取mutex,即便它看起來是解鎖狀態,也不要嘗試自旋,而是排到等待隊列的尾部。

在飢餓模式下,有一個goroutine獲取到mutex鎖了,如果它滿足下條件中的任意一個,mutex將會切換回去正常模式:

  1. 等待隊列只有一個goroutine
  2. goroutine的等待時間小於1ms

正常模式有更好的性能,因爲goroutine可以連續多次獲得mutex鎖,以及避免多個線程的排隊消耗。
飢餓模式需要預防隊列尾部goroutine一致無法獲取mutex鎖的問題。

mutex數據結構以及調用的函數

type Mutex struct {
    state int32 //將一個32位整數拆分爲:從最高位排列
    //當前阻塞的goroutine數(29位)
    //飢餓狀態(1位)
    //喚醒狀態(1位)
    //鎖狀態(1位) 
    sema uint32 // 信號量
}

const (
    mutexLocked = 1 << iota // 用最後一位表示當前鎖的狀態,0-未鎖住 1-已鎖住
    mutexWoken // 用倒數第二位表示當前鎖是否被喚醒 0-喚醒 1-未喚醒
    mutexStarving // 用倒數第三位表示當前鎖是否爲飢餓模式,0爲正常模式,1爲飢餓模式。
    mutexWaiterShift = iota // 3,從倒數第四位往前的bit位表示在排隊等待的goroutine數
    starvationThresholdNs = 1e6 // 1ms
)
  1. runtime_canSpin。判斷是否需要自選,golang中自旋鎖並不會一直自旋下去,在runtime包中runtime_canSpin方法做了一些限制, 傳遞過來的iter大等於4或者cpu核數小等於1,最大邏輯處理器大於1(多核),至少有個本地的P隊列,並且本地的P隊列可運行G隊列爲空纔會進行自旋。
func sync_runtime_canSpin(i int) bool {
	 if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
	 return false
	 }
	 if p := getg().m.p.ptr(); !runqempty(p) {
	 return false
	 }
	 return true
}
  1. runtime_doSpin。 會調用procyield函數,該函數也是彙編語言實現。函數內部[循環]調用PAUSE指令。PAUSE指令什麼都不做,但是會消耗CPU時間,在執行PAUSE指令時,CPU不會對它做不必要的優化。
func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}
  1. runtime_SemacquireMutex。 一個gotoutine的等待隊列,如果lifo爲true,則插入隊列頭,否則插入隊尾
func runtime_SemacquireMutex(s *uint32, lifo bool)
  1. runtime_Semrelease。喚醒被runtime_SemacquireMutex函數掛起的等待goroutine,如果handoff爲true,喚醒隊列頭第一個等待者,否則的話可能是隨機
func runtime_Semrelease(s *uint32, handoff bool)

Lock方法實現

Lock方法申請對mutex加鎖,Lock執行的時候,分三種情況:

  1. 無衝突。通過CAS操作把當前狀態設置爲加鎖狀態。
  2. 有衝突。開始runtime_canSpin自旋,並等待鎖釋放,如果其他goroutine在這段時間內釋放了該鎖,直接獲得該鎖;如果沒有釋放進入第3步。
  3. 有衝突,且已經過了自旋階段。通過調用seamacquire函數來讓當前goroutine進入等待狀態。
// 無衝突
// CompareAndSwapInt32相等才賦值,且返回true
// 查看 state 是否爲0(空閒狀態), 如果是則表示可以加鎖,將其鎖置爲鎖定狀態
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled { // 數據競爭檢測
			race.Acquire(unsafe.Pointer(m))
		}
		return
}

var waitStartTime int64  // 當前goroutine開始等待時間
starving := false        // goroutine當前所處的模式
awoke := false           // 當前goroutine是否被喚醒 
iter := 0                // 自旋迭代的次數
old := m.state           // old 保存當前 mutex 的狀態


for {
	// 有衝突,開始自旋狀態
	// 當mutex處於鎖定非飢餓工作模式且支持自旋操作的時候。其實就是在自旋然後等待別人釋放鎖,如果有人釋放鎖,則會立刻進行下面的嘗試獲取鎖的邏輯
	if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
		// 將 mutex.state 的倒數第二位設置爲1,用來告知 Unlock 操作,存在 goroutine 即將得到鎖,不需要喚醒其他goroutine
		// 當前線程沒有處於喚醒狀態,當前鎖處於喚醒狀態,當前有正在等待的goroutine
		if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
			atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
			awoke = true
		}
		// 進入自旋
		runtime_doSpin()
		iter++
		old = m.state
		continue
	}
	
	// 有衝突,且已經過了自旋階段。走到這裏出現三種情況:1. 當前鎖是未鎖定狀態;2. 鎖是飢餓模式;3. 自旋超過指定的次數,不再允許自旋。
	
	new := old
	// 1. 如果當前不是飢餓模式,則這裏其實就可以嘗試進行鎖的獲取了|=其實就是將鎖的那個bit位設爲1表示鎖定狀態
	if old&mutexStarving == 0 {
		new |= mutexLocked
	}
	// 當mutex處於加鎖或飢餓狀態的時候,新到來的goroutine進入等待隊列
    // 2.此處需要判斷是否爲加鎖狀態,因爲從1到2的時候可能mutex 重新被其他goroutine加鎖了
	if old&(mutexLocked|mutexStarving) != 0 {
		new += 1 << mutexWaiterShift //等待隊列加1操作
	}
	
	// 如果當前goroutine已經處於飢餓狀態,並且當前鎖還是被佔用,則鎖嘗試進行飢餓模式的切換,但如果當前 mutex 未鎖定,則不需要切換。Unlock操作希望飢餓模式存在等待者
	// 3.starving條件是爲了防止: 如果在2處判斷mutex沒有處於加鎖,而在這裏判斷mutex卻加鎖了,這時候加入飢餓模式,可是goroutine沒有入列
	if starving && old&mutexLocked != 0 {
		new |= mutexStarving
	}
	// awoke爲true則表明當前線程在上面自旋的時候,修改mutexWoken狀態成功,清除喚醒標誌位
	// 爲什麼要清除標誌位呢?實際上是因爲後續流程很有可能當前線程會被掛起,就需要等待其他釋放鎖的goroutine來喚醒
	// 但如果unlock的時候發現mutexWoken的位置不是0,則就不會去喚醒,則該線程就無法再醒來加鎖
	if awoke {
		if new&mutexWoken == 0 {
			throw("sync: inconsistent mutex state")
		}
		// &^操作:後面對應的位爲1,則清0前面對應的位,若後面對應的位爲0,則前面對應的位保持不變
		new &^= mutexWoken
	}
	// 調用CAS更新state狀態
	if atomic.CompareAndSwapInt32(&m.state, old, new) {
		 // 如果原來的狀態等於0則表明當前已經釋放了鎖並且也不處於飢餓模式下,表明可以加鎖成功,退出CAS
		if old&(mutexLocked|mutexStarving) == 0 {
			break 
		}
		// 排隊邏輯,如果發現waitStatrTime不爲0,則表明當前線程之前已經在排隊來,後面可能因爲unlock被喚醒,但是本次依舊沒獲取到鎖,所以就將它移動到等待隊列的頭部
		queueLifo := waitStartTime != 0
		if waitStartTime == 0 {
			// 記錄開始等待時間
			waitStartTime = runtime_nanotime()
		}
		// 將被喚醒卻沒得到鎖的 goroutine 插入當前等待隊列的最前端
		runtime_SemacquireMutex(&m.sema, queueLifo, 1)
		// 如果當前goroutine等待時間超過starvationThresholdNs,mutex 進入飢餓模式
		starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
		// 重新獲取狀態
		old = m.state
		// 如果發現當前已經是飢餓模式,注意飢餓模式喚醒的是第一個goroutine
		if old&mutexStarving != 0 {
			// 一致性檢查
			if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
				throw("sync: inconsistent mutex state")
			}
			delta := int32(mutexLocked - 1<<mutexWaiterShift)
			 // 如果不是飢餓模式了或者當前等待着只剩下一個,退出飢餓模式
			if !starving || old>>mutexWaiterShift == 1 {
				delta -= mutexStarving
			}
			// 更新狀態
			atomic.AddInt32(&m.state, delta)
			break
		}
		awoke = true
		iter = 0
	} else {
		old = m.state
	}
}

if race.Enabled {
	race.Acquire(unsafe.Pointer(m))
}

Unlock方法實現

func (m *Mutex) Unlock() {
    if race.Enabled {
        _ = m.state
        race.Release(unsafe.Pointer(m))
    }
    // 直接進行cas操作: mutex 的state減去1, 加鎖狀態 -> 未加鎖
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if new != 0 {
	    if (new+mutexLocked)&mutexLocked == 0 {
	        throw("sync: unlock of unlocked mutex")
	    }
		// 正常模式釋放
	    if new&mutexStarving == 0 {
	        old := new
	        for {
				// 如果沒有等待者,或者已經存在一個 goroutine 被喚醒或得到鎖,或處於飢餓模式,無需喚醒任何處於等待狀態的 goroutine
				// 因爲lock方法存在自旋一直在獲取鎖,所以可能解鎖後就已經有goroutine獲取到鎖了
	            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
	                return
	            }
	            // 減去一個等待計數,然後將當前模式切換成mutexWoken
	            new = (old - 1<<mutexWaiterShift) | mutexWoken
	            if atomic.CompareAndSwapInt32(&m.state, old, new) {
	               // 隨機喚醒一個阻塞的 goroutine
	                runtime_Semrelease(&m.sema, false)
	                return
	            }
	            old = m.state
	        }
	    // 飢餓模式喚醒
	    } else {
	        // 喚醒第一個等待的線程
	        runtime_Semrelease(&m.sema, true)
	    }
    } 
}

參考文獻

golang mutex源碼詳細解析
golang之sync.Mutex互斥鎖源碼分析
go sync.Mutex 設計思想與演化過程 (一)
sync包 mutex源碼閱讀
圖解Go裏面的互斥鎖mutex瞭解編程語言核心實現源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章