Go Mutex 源碼學習

概述

互斥鎖是併發程序中對共享資源進行訪問控制的主要手段,Mutex是go語言提供的簡單易用的互斥鎖。Mutex的結構很簡單,暴露的方法也只有2個,一個加鎖 一個解鎖。那麼我們每天用的Mutex互斥鎖是如何實現的呢?
其實使用的是go語言automic包中的院子操作,具體如何使用可以參考之前寫的文章
在Mutex中的state是狀態碼,在mutex中把state分成4段。如下圖:
圖片描述

  • Locked:表示是否上鎖 上鎖爲1 未上鎖爲0
  • Woken:表示是否被喚醒,喚醒爲1 未喚醒爲0
  • Starving:表示是否爲飢餓模式,飢餓模式爲1 非飢餓模式爲0
  • waiter:剩餘的29位則爲等待的goroutine數量

互斥鎖的實現其實就是爭奪Locked,當goroutineA 搶到了鎖之後,第二個GoroutineB獲取鎖則會被阻塞等到GoroutineA釋放鎖之後GoroutineB將會被喚醒。當然具體實現則不會有這麼簡單,其中還有飢餓模式,自旋函數等一些概念。

前置概念

自旋

什麼是自旋

加鎖時,如果當前Locked位爲1,說明該鎖當前由其他協程持有,嘗試加鎖的協程並不是馬上轉入阻塞,而是會持續的探測Locked位是否變爲0,這個過程即爲自旋過程。自旋時間很短,但如果在自旋過程中發現鎖已被釋放,那麼協程可以立即獲取鎖。此時即便有協程被喚醒也無法獲取鎖,只能再次阻塞。
自旋的好處是,當加鎖失敗時不必立即轉入阻塞,有一定機會獲取到鎖,這樣可以避免協程的切換。

自旋的問題

如果自旋過程中獲得鎖,則馬上執行該goroutine。如果永遠在自旋模式中那麼之前阻塞的goroutine則很難獲得鎖,這樣一來一些goroutine則會被阻塞時間過長。如何解決這個問題,go mutex中引入了兩種模式,具體請看下文。

Mutex的兩種模式

普通模式

在普通模式下等待者以 FIFO 的順序排隊來獲取鎖,但被喚醒的等待者發現並沒有獲取到 mutex,並且還要與新到達的 goroutine 們競爭 mutex 的所有權。

飢餓模式

在飢餓模式下,mutex 的所有權直接從對 mutex 執行解鎖的 goroutine 傳遞給等待隊列前面的等待者。新到達的 goroutine 們不要嘗試去獲取 mutex,即使它看起來是在解鎖狀態,也不要試圖自旋。

源碼分析

Mutex對象

type Mutex struct {
    // 狀態碼
    state int32
    // 信號量,用於向處於 Gwaitting 的 G 發送信號
    sema  uint32
}

const(
    // 值=1 表示是否鎖住 1=鎖 0=未鎖
    mutexLocked = 1 << iota // mutex is locked
    // 值=2 表示是否被喚醒 1=喚醒  0=未喚醒
    mutexWoken
    // 是否爲飢渴模式(等待超過1秒則爲飢渴模式)
    mutexStarving
    // 右移3位,爲等待的數量
    mutexWaiterShift = iota
    // 飢餓模式的時間 
    starvationThresholdNs = 1e6
)

加鎖

func (m *Mutex) Lock() {
    // 利用atomic包中的cas操作判斷是否上鎖
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        // 判斷是否啓用了race檢測
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        // m.state = 1 直接返回 其他goroutine調用lock會發現已經被上鎖
        return
    }
    
    // 等待時間
    var waitStartTime int64
    // 飢餓模式標誌位
    starving := false
    // 喚醒標誌位
    awoke := false
    // 自旋迭代的次數
    iter := 0
    // 保存 mutex 當前狀態
    old := m.state
    // 循環
    for {
        // 判斷 如果不是飢餓模式並且是否能夠執行自旋函數(判斷自旋次數)
        // old&(0001|0100) == 0001 ==> old&0101 
        // 當old爲0001爲非飢餓模式  0001 == 0001 true     當old爲0101飢餓模式  0101 == 0001 false  
        // runtime_canSpin 判斷自旋少於4次,並且是多核機器上並且GOMAXPROCS>1
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // 判斷條件:
            // 未被喚醒 && 等待數量不爲0 && 使用CAS設置狀態爲已喚醒 
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                // 設置激活爲true
                awoke = true
            }
            // 自旋函數 自旋次數+1
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }
        
        // 如果不能執行自旋函數 記錄一個new狀態 然後判斷改變new 最終使用CAS替換嘗試設置state屬性
        new := old
        // 當前的mutex.state處於正常模式,則將new的鎖位設置爲1
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }
        // 如果當前鎖鎖狀態爲鎖定狀態或者處於飢餓模式,則將等待的線程數量+1
        if old&(mutexLocked|mutexStarving) != 0 {
            new += 1 << mutexWaiterShift
        }
        // 如果starving變量爲true並且處於鎖定狀態,則new的飢餓狀態位打開
        if starving && old&mutexLocked != 0 {
            new |= mutexStarving
        }
        // 對於狀態的驗證
        if awoke {
            // The goroutine has been woken from sleep,
            // so we need to reset the flag in either case.
            if new&mutexWoken == 0 {
                throw("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }
        // new已經判斷設置完,如果mutex的state沒有變動過的話 則替換成new 
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果未被鎖定並且並不是出於飢餓狀態 退出循環 goroutine獲取到鎖
            if old&(mutexLocked|mutexStarving) == 0 {
                break // locked the mutex with CAS
            }
            // 如果當前的 goroutine 之前已經在排隊了,就排到隊列的前面。
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            // 進入休眠狀態,等待信號喚醒後重新開始循環 如果queueLifo爲true,則將等待goroutine插入到隊列的前面
            runtime_SemacquireMutex(&m.sema, queueLifo)
            // 計算等待時間 確定 mutex 當前所處模式
            // 此時這個goroutine已經被喚醒 
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            old = m.state
            // 判斷被喚醒的goroutine是否爲飢餓狀態
            if old&mutexStarving != 0 {
                // If this goroutine was woken and mutex is in starvation mode,
                // ownership was handed off to us but mutex is in somewhat
                // inconsistent state: mutexLocked is not set and we are still
                // accounted as waiter. Fix that.
                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
            }
            // 如果不是飢餓模式 讓新到來的 goroutine 先獲取鎖,繼續循環
            awoke = true
            iter = 0
        } else {
            // 如果CAS替換未能成功 則繼續循環
            old = m.state
        }
    }

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

解鎖

func (m *Mutex) Unlock() {
    if race.Enabled {
        _ = m.state
        race.Release(unsafe.Pointer(m))
    }

    // 利用原子操作 設置state鎖位置爲0
    new := atomic.AddInt32(&m.state, -mutexLocked)
    // 判斷狀態,給未加鎖的mutex解鎖,拋出錯誤
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }
    // 判斷是否爲飢餓模式
    if new&mutexStarving == 0 {
        // 正常狀態
        old := new
        for {
            // 如果等待的goroutine爲零 || 已經被鎖定、喚醒、或者已經變成飢餓狀態
            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // 更新new的值,減去等待數量
            new = (old - 1<<mutexWaiterShift) | mutexWoken
            // 使用CAS 替換舊值
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果替換成功 則恢復掛起的goroutine.r如果爲 true表明將喚醒第一個阻塞的goroutine
                runtime_Semrelease(&m.sema, false)
                return
            }
            old = m.state
        }
    } else {
        // 恢復掛起的goroutine.r如果爲 true表明將喚醒第一個阻塞的goroutine
        runtime_Semrelease(&m.sema, true)
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章