【golang】sync.Mutex反発ロックの実現原理


sync.Mutexは再入できない排他ロックです.これはJavaとは異なり、golangの中の排他ロックは再入力できません.
1つのgoroutineがこのロックの所有権を取得すると、他のロックを要求するgoroutineはロックメソッドの呼び出しにブロックされ、ロックが解放されるまでブロックされる.
データ構造とステータスマシン
sync.Mutexは2つのフィールドstateとsemaからなる.ここでstateは現在の反発ロックの状態を表し、semaはロック状態を制御するための信号量である.
type Mutex struct {
    state int32
    sema  uint32
}


Mutexはいったん使用した後、必ずcopy操作をしないことを強調する必要があります.
Mutexのステータスマシンは複雑で、int 32を使用して表します.
const (
    mutexLocked = 1 << iota // mutex is locked
    mutexWoken  //2
    mutexStarving //4
    mutexWaiterShift = iota //3
)
                                                                                             
32                                               3             2             1             0 
 |                                               |             |             |             | 
 |                                               |             |             |             | 
 v-----------------------------------------------v-------------v-------------v-------------+ 
 |                                               |             |             |             v 
 |                 waitersCount                  |mutexStarving| mutexWoken  | mutexLocked | 
 |                                               |             |             |             | 
 +-----------------------------------------------+-------------+-------------+-------------+                                                                                                              


最低3位はmutexLocked、mutexWoken、mutexStarvingで、残りの位置は現在、相互反発ロックの解放を待っているGoroutineがどれだけあるかを示します.
デフォルトでは、反発ロックのすべての状態ビットは0であり、int 32の異なる同位体はそれぞれ異なる状態を表す.
  • mutexLocked-反発ロックのロック状態を表します.
  • mutexWoken-通常モードから起動されたことを示します.
  • mutexStarving-現在の反発ロックは飢餓状態に入っています.
  • waitersCount-現在の反発ロックで待機しているgoroutineの数.

  • ロックの公平性を保証するために、設計上の反発ロックには2つの状態がある:正常状態と飢餓状態. では、すべての待機ロックのgoroutineがFIFO順に待機する.起動したgoroutineは、ロックを直接所有するのではなく、新しいロックを要求したgoroutineと競合します.新しいロックを要求するgoroutineは、CPU上で実行されており、いくつかある可能性があるため、起動したばかりのgoroutineはロック競合で失敗する可能性が高いという利点があります.この場合、この起動されたgoroutineは待機キューの前に追加されます. goroutine 1ms , . では、ロックの所有権はunlockのgorutineから待機キューの最初のものに直接渡されます.新しく来たgoroutineは、ロックを取得しようとはしません.ロックがunlock状態に見えても、スピン操作を試みるのではなく、待機キューの末尾に配置されます.
    待機中のgoroutineがロックを取得し、(1)キューの最後の条件である.(2)待ち時間が1 ms未満である.ロックの状態を通常の状態に変換します.
    正常な状態は良好な性能表現があり、飢餓モードも非常に重要であり、尾の遅延現象を阻止することができるからだ.
    Lock
    func (m *Mutex) Lock() {
        //   mutex state    ,     /   goroutine,        ,     ,  .
        //        goroutine   ,      。          ,      。
        if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
            return
        }
        // Slow path (outlined so that the fast path can be inlined)
        m.lockSlow()
    }
    
    func (m *Mutex) lockSlow() {
        //    goroutine     
        var waitStartTime int64
        //  goroutine          
        starving := false
        //  goroutine     
        awoke := false
        //     
        iter := 0
        old := m.state
        for {
            //      :1.mutex     ;2.       (       ,       ,                  。)
            //        :  runtime_canSpin  
            if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
                //            
                //           state     woken  ,     woken  ,          。
                if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                    atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                    awoke = true
                }
                runtime_doSpin()
                iter++
                old = m.state
                continue
            }
            
            //      , state      :
            // 1.        ,       
            // 2.        ,        
            // 3.       ,        
            // 4.       ,        
            //    gorutine  awoke   true,     false (  goutine     state woken  )
            
            // new    state     ,         
            // old        
            new := old
            
            //   old state        , new state    ,     CAS   ,
            //   old state       ,     new state  ,                    .
            if old&mutexStarving == 0 {
                new |= mutexLocked
            }
            //              1
            if old&(mutexLocked|mutexStarving) != 0 {
                new += 1 << mutexWaiterShift
            }
            
            //     goroutine        ,   old state     ,
            //  new state          ,          .
            if starving && old&mutexLocked != 0 {
                new |= mutexStarving
            }
            
             //    goroutine         ,     new state     ,    goroutine      ,      ,
            //   state       woken  .
            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
            }
    
            //   CAS  new state .
            //   new        true,            state     .
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                
                //   old state         ,          ,
                //     goroutine          ,  
                if old&(mutexLocked|mutexStarving) == 0 {
                    break // locked the mutex with CAS
                }
                // If we were already waiting before, queue at the front of the queue.
                //       goroutine     
                queueLifo := waitStartTime != 0
                if waitStartTime == 0 {
                    waitStartTime = runtime_nanotime()
                }
                //         ,      sleep     goroutine
                //       goroutine,queueLifo=false,           ,    
                //       goroutine, queueLifo=true,           
                runtime_SemacquireMutex(&m.sema, queueLifo, 1)
    
                // sleep  , goroutine   
                //     goroutine          .
                starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
                //         
                old = m.state
    
                //      state       
                //        Unlock  ,             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")
                    }
                    //   goroutine     ,     goroutine  1.
                    delta := int32(mutexLocked - 1<>mutexWaiterShift == 1 {
                         //       
                        delta -= mutexStarving
                    }
                    //    state,         ,  、  
                    atomic.AddInt32(&m.state, delta)
                    break
                }
                awoke = true
                iter = 0
            } else {
                old = m.state
            }
        }
    }
    
    

    全体の過程は複雑で、ここでいくつかの重点をまとめます.
  • ロックが初期状態(unlock、通常モード)の場合、CAS(0->Locked)によってロックが取得される.取得に失敗した場合は、slowLockのプロセス:
  • に進みます.
    slowLockのロック取得プロセスには、飢餓モードと正常モードの2つのモードがあります.
    (1)通常モード
  • mutexはすでにlockedされ、正常なモードにある.
  • 前Goroutineは、このロックを取得するためにスピンに入る回数が4回未満である.
  • 現在の機械CPUコア数は1より大きい.
  • 現在のマシンには少なくとも1つの実行中のプロセッサPが存在し、処理された実行キューは空である.

  • 上記の4つの条件を満たすgoroutineこそスピンを作ることができる.スピンはsync.runtimeを呼び出しますdoSpinとruntime.procyieldは、CPUを占有しCPU時間を消費するだけのPAUSE命令を30回実行します.
    スピン関連の特殊な論理を処理すると,反発ロックはコンテキストに基づいて現在の反発ロックの最新の状態newを計算する.stateフィールドに格納されている異なる情報(mutexLocked、mutexStarving、mutexWoken、mutexWaiterShift)は、いくつかの異なる条件で更新されます.
    最新のnewを計算した後、CASは更新に成功し、old状態がロックされていない状態であり、ロックが飢餓状態でない場合、現在のgoroutine競合が成功し、ロックの戻りを取得したことを意味します.(これは、現在goroutineが通常モードで競合している場合にロックが容易になる理由です)
    現在のgoroutine競合が失敗した場合、sync.runtime_SemacquireMutexが呼び出され、信号量を使用してリソースが2つのGoroutineによって取得されないことを保証します.sync.runtime_SemacquireMutexは、現在のGoroutine待ち信号量の解放をメソッドで呼び出し続け、現在のGoroutineが信号量を取得できるとすぐに戻り、sync.Mutex.Lockメソッドの残りのコードも実行し続けます.
    (2)飢餓モード
    飢餓モード自体は公平性をある程度保証するために設計されたモードである.したがって、飢餓モードではスピンの操作は行われず、新しいGoroutineはこの状態でロックを取得することもスピン状態に入ることもできず、キューの最後で待つだけです.
    通常モードでも飢餓モードでも、信号量を取得すると、ブロックからすぐに戻り、残りのコードを実行します.
  • 通常モードでは、このコードは、起動および飢餓フラグを設定し、反復回数をリセットし、ロックを取得するサイクルを再実行します.
  • 飢餓モードでは、現在のGoroutineは反発ロックを取得し、待機キューに現在のGoroutineのみが存在する場合、反発ロックは飢餓モードから終了する.

  • Unlock
    func (m *Mutex) Unlock() {
        // Fast path: drop lock bit.
        new := atomic.AddInt32(&m.state, -mutexLocked)
        if new != 0 {
            // Outlined slow path to allow inlining the fast path.
            // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
            m.unlockSlow(new)
        }
    }
    
    func (m *Mutex) unlockSlow(new int32) {
        if (new+mutexLocked)&mutexLocked == 0 {
            throw("sync: unlock of unlocked mutex")
        }
        if new&mutexStarving == 0 {
            old := new
            for {
                // If there are no waiters or a goroutine has already
                // been woken or grabbed the lock, no need to wake anyone.
                // In starvation mode ownership is directly handed off from unlocking
                // goroutine to the next waiter. We are not part of this chain,
                // since we did not observe mutexStarving when we unlocked the mutex above.
                // So get off the way.
                if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                    return
                }
                // Grab the right to wake someone.
                new = (old - 1<

    反発ロックのロック解除プロセスsync.Mutex.Unlockはロック解除プロセスと比較して簡単で、AddInt 32関数を使用してロックを迅速に解除すると、次の2つの状況が発生します.
  • この関数が返す新しい状態が0に等しい場合、現在のGoroutineは反発ロックをロック解除することに成功する.
  • 関数が返す新しい状態が0に等しくない場合、このコードはsync.Mutex.unlockSlowメソッドを呼び出してスローロックを開始します:
  • sync.Mutex.unlockSlowメソッドは、まず、ロック状態の正当性を検証します.現在の反発ロックがロック解除された場合、異常sync:unlock of unlocked mutexが現在のプログラムを中止します.
    通常は、現在の反発ロックの状態に応じて、通常モードと飢餓モードの反発ロックをそれぞれ処理します.
  • 通常モードでは、このコードは以下の2つの状況処理をそれぞれ処理する.
  • 排他ロックに待機者または排他ロックが存在しないmutexLocked、mutexStarving、mutexWoken状態が0でない場合、現在の方法は直接戻ることができ、他の待機者を呼び覚ます必要はありません.
  • 相互反発ロックに待機者が存在する場合sync.runtime_Semreleaseは待機者を呼び覚まし、ロックの所有権を渡す.
  • 飢餓モードでは、上記のコードはsync.runtimeを直接呼び出します.Semreleaseメソッドは、現在のロックを次のロックを取得しようとしている待機者に渡し、待機者が起動するとロックされ、このとき反発ロックはまだ飢餓状態を脱退しない.