sync.Mutex
原理:一個共享的變量,哪個線程握到了,哪個線程可以執行代碼
功能:一個性能不錯的悲觀鎖,使用方式和Java的ReentrantLock很像,就是手動Lock,手動UnLock。
使用例子:
var mu sync.Mutex
var cnt int
func add() {cnt++}
var wg sync.WaitGroup// 管理協程用的,主要是讓協程同意結束后再運行調用協程
func main() {for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()defer mu.Unlock()mu.Lock()add()}()}wg.Wait()fmt.Print(cnt)
}
實現原理:
直接看源碼好了,
func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { // 能拿到鎖,就拿,拿不到就進入慢速模式if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path (outlined so that the fast path can be inlined)m.lockSlow()
}
lockSlow()
是一個邏輯很多的方法,具體邏輯是:
- 自旋嘗試:
- 若當前是正常模式且鎖持有時間較短,當前goroutine會自旋(循環檢查鎖狀態),嘗試避免立即阻塞。
- 自旋條件:多核CPU、當前未處于饑餓模式、等待隊列為空或自旋次數未超過閾值。
- 更新等待計數:
- 通過原子操作增加
state
中的等待goroutine計數(高30位)。
- 通過原子操作增加
- 進入阻塞或饑餓模式:
- 正常模式:若自旋失敗,將當前goroutine加入信號量等待隊列(
sema
),并調用runtime_SemacquireMutex
阻塞。 - 饑餓模式:若當前goroutine等待時間超過閾值(1ms),觸發饑餓模式。此時新來的goroutine直接進入隊列尾部,不再自旋。
- 正常模式:若自旋失敗,將當前goroutine加入信號量等待隊列(
func (m *Mutex) lockSlow() {// 初始化變量操作,省略...for {// 這部分處理自旋嘗試獲取鎖的邏輯if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// 省略...runtime_doSpin()continue}new := old// 如果不是饑餓模式,嘗試獲取鎖(new |= mutexLocked)if old&mutexStarving == 0 {new |= mutexLocked}// 如果鎖已被占用或處于饑餓模式,增加等待者計數(new += 1 << mutexWaiterShift)if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 如果當前 goroutine 處于饑餓狀態且鎖被占用,切換到饑餓模式(new |= mutexStarving)if starving && old&mutexLocked != 0 {new |= mutexStarving}// 如果當前 goroutine 是被喚醒的:確保 mutexWoken 標志已設置(否則拋出異常);清除 mutexWoken 標志(new &^= mutexWoken)if awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}// 嘗試用 CAS 更新鎖狀態if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break }// 決定排隊位置:如果是第一次等待(waitStartTime == 0),記錄開始等待時間;否則使用 LIFO 順序(queueLifo = true)queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// runtime_SemacquireMutex 將 goroutine 放入等待隊列并阻塞runtime_SemacquireMutex(&m.sema, queueLifo, 2)// 被喚醒后:檢查是否等待超時(超過 1ms),更新饑餓狀態;重新讀取鎖狀態starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// 如果是饑餓模式:if old&mutexStarving != 0 {// 檢查狀態是否一致if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}// 計算狀態增量: 設置 mutexLocked;減少等待者計數;如果不再饑餓或只有一個等待者,退出饑餓模式delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {delta -= mutexStarving}// 原子更新狀態并退出循環atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}
}
Goroutine A 獲取鎖(Lock()快速路徑成功)。
Goroutine B 嘗試獲取鎖,進入慢速路徑:
自旋數次后失敗,增加等待計數,進入隊列阻塞。
Goroutine A 釋放鎖(Unlock()):
喚醒Goroutine B,新來的Goroutine C可與B競爭鎖。
Goroutine B 等待超過1ms,觸發饑餓模式。
Goroutine C 新到達,直接進入隊列尾部,不自旋。
Goroutine A 釋放鎖:
直接將鎖交給隊列頭部的Goroutine B。
Goroutine B 釋放鎖后,若隊列中無等待者,退出饑餓模式。
作者:ShanekAI
鏈接:https://juejin.cn/post/7488246529430487077
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
整體邏輯大致來說就是:Go 的 sync.Mutex
在競爭不激烈時,會采用短暫的 自旋鎖 機制。自旋鎖允許 Goroutine 在一小段時間內忙等待,而不是立即進入阻塞狀態。這種策略避免了頻繁的上下文切換開銷。如果激烈的話,就進入饑餓模式,更改了邏輯,在饑餓模式里,停止自旋,直接將當前協程加入等待隊列。當前線程執行完畢了,如果是饑餓模式,會把隊列里第一個拿出來喚醒。
名詞解釋:
自旋:就是忙等,就是最簡單的例子:
for state == 1{} // 不停地遍歷,就好像在不停地自我旋轉一樣;直到state被其他線程修改了,才停止