Go sync.Mutex源码分析

Go 语言作为一个原生支持用户态进程(Goroutine)的语言,当提到并发编程、多线程编程时,往往都离不开锁这一概念。锁是一种并发编程中的同步原语(Synchronization Primitives),它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件(Race condition)等问题。

Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once 和 sync.Cond,下面介绍sync.Mutex的具体原理(基于go1.15,不同版本mutex实现不一致)

sync.Mutex是互斥锁,Lock和UnLock

sync.Mutex结构

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex. 
//
// A Mutex must not be copied after first use.
// NOTE: 加起来总共64位,占8字节,在64位机器字长下,可以原子性的操作
// NOTE: 一般而言,机器字长等于寄存器字长,也就是加法器一次能处理的最长数据
// NOTE: 内存/磁盘存储字长于编址相关,一般按字(B)编址
type Mutex struct {
   state int32 // 表示当前互斥锁的状态
   sema  uint32 // 用于控制锁的信号量
}

const (
   mutexLocked = 1 << iota // mutex is locked
   mutexWoken
   mutexStarving
   mutexWaiterShift = iota

   // Mutex fairness.
   // NOTE: 在Go 1.9中引入了fairness优化,防止部分Goroutine饿死
   //
   // Mutex can be in 2 modes of operations: normal and starvation.
   // NOTE: 互斥锁有两种模式: 正常和饥饿模式
   
   // In normal mode waiters are queued in FIFO order, but a woken up waiter
   // does not own the mutex and competes with new arriving goroutines over
   // the ownership. New arriving goroutines have an advantage -- they are
   // already running on CPU and there can be lots of them, so a woken up
   // waiter has good chances of losing. In such case it is queued at front
   // of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
   // it switches mutex to the starvation mode.
   // NOTE: 正常模式下,waiters出等待队列遵循先入先出的顺序,但并发场景下,新来的goroutine会比刚唤醒的goroutine在抢锁上更有优势
   // 因为新来的goroutine已经占据cpu时间片,而刚唤醒的goroutine正在等待调度或切换上下文,锁容易被抢
   // 所以当一个waiters超过1ms都没抢到锁,会将锁的模式转换为饥饿模式
   //
   // In starvation mode ownership of the mutex is directly handed off from
   // the unlocking goroutine to the waiter at the front of the queue.
   // New arriving goroutines don't try to acquire the mutex even if it appears
   // to be unlocked, and don't try to spin. Instead they queue themselves at
   // the tail of the wait queue.
   // NOTE: 饥饿模式下,当一个锁释放时,会直接交给等待队列头部的waiters,新来的goroutine在该模式下无法抢锁,也无法自旋,只能被加入
   // 到等待队列尾部
   //
   // If a waiter receives ownership of the mutex and sees that either
   // (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
   // it switches mutex back to normal operation mode.
   // NOTE: 如果一个waiter获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
   //
   // Normal mode has considerably better performance as a goroutine can acquire
   // a mutex several times in a row even if there are blocked waiters.
   // Starvation mode is important to prevent pathological cases of tail latency.
   // NOTE:正常模式相比于饥饿模式有更好的性能
   starvationThresholdNs = 1e6 // 1ms
)

互斥锁的状态

  1. mutexLocked — 表示互斥锁的锁定状态 (1 << 0);
  2. mutexWoken — 表示当前是否有goroutine被唤醒 (1 << 1);
  3. mutexStarving — 表示当前的互斥锁是否进入饥饿状态 (1 << 2);
  4. waitersCount — 当前互斥锁上等待的 Goroutine 个数 (其余29位);
    在这里插入图片描述

互斥锁加锁操作

// Lock locks m.
// If the lock is already in use, the calling goroutine
// blocks until the mutex is available.
// NOTE: 加锁,抢不到就阻塞直到锁可获取,加锁有两种方式
func (m *Mutex) Lock() {
   // Fast path: grab unlocked mutex.
   // NOTE: Fast path快速加锁方法
   // 通过原子性的CAS判断,如果锁的state为0,即互斥锁未被占用,那么将其抢占后直接返回
   if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
       // NOTE: 竞态检测, 用于检测当前是否有其他操作同时操纵此Mutex对象, 没有加上-race时不触发
      if race.Enabled {
         race.Acquire(unsafe.Pointer(m))
      }
      return
   }
   
   // Slow path (outlined so that the fast path can be inlined)
   // NOTE: 其他情况都走Slow path模式,尝试通过自旋(Spinnig)等一系列方式等待锁的释放
   m.lockSlow()
}

// Slow path lock
func (m *Mutex) lockSlow() {
   var waitStartTime int64
   starving := false
   awoke := false
   iter := 0
   old := m.state
   for {
      // Don't spin in starvation mode, ownership is handed off to waiters
      // so we won't be able to acquire the mutex anyway.
      // NOTE: 在正常模式下,如果锁已经被人占用,并且可以通过自旋检测,那么通过自旋等待互斥锁的释放
      // NOTE: 自旋是一种多线程同步机制,当前的进程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。
      // 在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序
      // 所以 Goroutine 进入自旋的条件非常苛刻,条件如下:
      // 1. 只有在普通模式下,才允许自旋
      // 2. 通过runtime_canSpin检查
      // 2.1 运行在多 CPU 的机器上
      // 2.2 当前 Goroutine 为了获取该锁进入自旋的次数小于四次
      // 2.3 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空
      if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
         // Active spinning makes sense.
         // Try to set mutexWoken flag to inform Unlock
         // to not wake other blocked goroutines.
         if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
            atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
            awoke = true
         }
         
         // NOTE: 通过runtime_doSpin进入自旋, 分析见1.1
         runtime_doSpin()
         iter++
         old = m.state
         continue
      }
      
      // NOTE: 根据上下文,计算当前互斥锁最新状态,相当于构造当前环境下理想的state变量
      new := old
      // Don't try to acquire starving mutex, new arriving goroutines must queue.
      // NOTE: 如果不在饥饿模式,则获取锁,将new的bit 0置为1
      if old&mutexStarving == 0 {
         new |= mutexLocked
      }
      // NOTE: 增加当前等待锁的goroutine个数
      if old&(mutexLocked|mutexStarving) != 0 {
         new += 1 << mutexWaiterShift
      }
      // The current goroutine switches mutex to starvation mode.
      // But if the mutex is currently unlocked, don't do the switch.
      // Unlock expects that starving mutex has waiters, which will not
      // be true in this case.
      // NOTE: 如果后续判断为饥饿模式,并且锁已经被占用,尝试将最新状态转换为饥饿模式
      if starving && old&mutexLocked != 0 {
         new |= mutexStarving
      }
      // NOTE: 如果当前waiter处于唤醒状态, 将最新状态的mutexWoken置为1
      // 防止唤醒其他等待锁并且此时处于阻塞状态的goroutine,增加无意义的竞争
      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
      }
      
      // NOTE: 尝试通过CAS的方式,更新state状态
      if atomic.CompareAndSwapInt32(&m.state, old, new) {
          // NOTE: 如果上一次的state,锁已经没人占用并且不处于饥饿模式,那么意味着通过CAS更新的state抢到了锁
         if old&(mutexLocked|mutexStarving) == 0 {
            break // locked the mutex with CAS
         }
         
         // If we were already waiting before, queue at the front of the queue.
         // NOTE: 检查是否非第一次等待
         queueLifo := waitStartTime != 0
         if waitStartTime == 0 {
            waitStartTime = runtime_nanotime()
         }
         // NOTE: runtime_SemacquireMutex 是runtime提供的用于获取信号量的方法,相当于PV操作的P
         // NOTE: 如果没有获取到信号量,当前的goroutine会进入阻塞状态,直到被唤醒
         // NOTE: 具体分析见1.2
         runtime_SemacquireMutex(&m.sema, queueLifo, 1)
         
         // NOTE: 此处是唤醒后执行
         // NOTE: 当唤醒后,计算获取锁的时间是否超过阈值,当前是否应该进入饥饿状态
         starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
         old = m.state
         
         // NOTE: 在正常模式下,会设置唤醒和饥饿标记、重置迭代次数并重新执行获取锁的循环
         // NOTE: 如果已经位于饥饿状态, 当前 Goroutine 会获得互斥锁
         // 如果等待队列中只存在当前 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 {
               // Exit starvation mode. 退出饥饿模式
               // Critical to do it here and consider wait time.
               // Starvation mode is so inefficient, that two goroutines
               // can go lock-step infinitely once they switch mutex
               // to starvation mode.
               delta -= mutexStarving
            }
            atomic.AddInt32(&m.state, delta)
            break
         }
         awoke = true
         iter = 0
      } else {
         old = m.state
      }
   }

   if race.Enabled {
      race.Acquire(unsafe.Pointer(m))
   }
}
// 1.1
// NOTE: 一旦当前 Goroutine 能够进入自旋就会调用runtime.procyield 执行 30 次的 PAUSE 指令
// 该指令只会占用 CPU 并消耗 CPU 时间

func sync_runtime_doSpin() {
	procyield(active_spin_cnt)
}

TEXT runtime·procyield(SB),NOSPLIT,$0-0
	MOVL	cycles+0(FP), AX
again:
	PAUSE
	SUBL	$1, AX
	JNZ	again
	RET
// 1.2 sync/runtime.go

// SemacquireMutex is like Semacquire, but for profiling contended Mutexes.
// If lifo is true, queue waiter at the head of wait queue.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_SemacquireMutex's caller.
// NOTE: 如果lifo为true,意味着不是第一次被唤醒,那么会将其插入树堆某个信号量的等待链表中
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)


// 1.3 runtime/sema.go

//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
   semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}

func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) {
   gp := getg()
   if gp != gp.m.curg {
      throw("semacquire not on the G stack")
   }

   // Easy case.
   if cansemacquire(addr) {
      return
   }

   // Harder case:
   // increment waiter count
   // try cansemacquire one more time, return if succeeded
   // enqueue itself as a waiter
   // sleep
   // (waiter descriptor is dequeued by signaler)
   s := acquireSudog()
   root := semroot(addr)
   t0 := int64(0)
   s.releasetime = 0
   s.acquiretime = 0
   s.ticket = 0
   if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
      t0 = cputicks()
      s.releasetime = -1
   }
   if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
      if t0 == 0 {
         t0 = cputicks()
      }
      s.acquiretime = t0
   }
   for {
      lockWithRank(&root.lock, lockRankRoot)
      // Add ourselves to nwait to disable "easy case" in semrelease.
      atomic.Xadd(&root.nwait, 1)
      // Check cansemacquire to avoid missed wakeup.
      if cansemacquire(addr) {
         atomic.Xadd(&root.nwait, -1)
         unlock(&root.lock)
         break
      }
      // Any semrelease after the cansemacquire knows we're waiting
      // (we set nwait above), so go to sleep.
      // NOTE: 入队列
      root.queue(addr, s, lifo)
      // NOTE: goparkunlock 将当前goroutine修改为阻塞/等待state 
      goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
      if s.ticket != 0 || cansemacquire(addr) {
         break
      }
   }
   if s.releasetime > 0 {
      blockevent(s.releasetime-t0, 3+skipframes)
   }
   releaseSudog(s)
}

// Puts the current goroutine into a waiting state and unlocks the lock.
// The goroutine can be made runnable again by calling goready(gp).
func goparkunlock(lock *mutex, reason waitReason, traceEv byte, traceskip int) {
   gopark(parkunlock_c, unsafe.Pointer(lock), reason, traceEv, traceskip)
}

// queue adds s to the blocked goroutines in semaRoot.
func (root *semaRoot) queue(addr *uint32, s *sudog, lifo bool) {
   s.g = getg()
   s.elem = unsafe.Pointer(addr)
   s.next = nil
   s.prev = nil

   var last *sudog
   pt := &root.treap
   for t := *pt; t != nil; t = *pt {
      if t.elem == unsafe.Pointer(addr) {
         // Already have addr in list.
         // NOTE: 如果lifo为true,意味着唤醒的goroutine已经等待过,所以入等待队列头
         if lifo {
            // Substitute s in t's place in treap.
            *pt = s
            s.ticket = t.ticket
            s.acquiretime = t.acquiretime
            s.parent = t.parent
            s.prev = t.prev
            s.next = t.next
            if s.prev != nil {
               s.prev.parent = s
            }
            if s.next != nil {
               s.next.parent = s
            }
            // Add t first in s's wait list.
            s.waitlink = t
            s.waittail = t.waittail
            if s.waittail == nil {
               s.waittail = t
            }
            t.parent = nil
            t.prev = nil
            t.next = nil
            t.waittail = nil
         } else {
         // NOTE: 如果lifo为false,入等待队列尾部
            // Add s to end of t's wait list.
            if t.waittail == nil {
               t.waitlink = s
            } else {
               t.waittail.waitlink = s
            }
            t.waittail = s
            s.waitlink = nil
         }
         return
      }
      last = t
      if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) {
         pt = &t.prev
      } else {
         pt = &t.next
      }
   }

   // Add s as new leaf in tree of unique addrs.
   // The balanced tree is a treap using ticket as the random heap priority.
   // That is, it is a binary tree ordered according to the elem addresses,
   // but then among the space of possible binary trees respecting those
   // addresses, it is kept balanced on average by maintaining a heap ordering
   // on the ticket: s.ticket <= both s.prev.ticket and s.next.ticket.
   // https://en.wikipedia.org/wiki/Treap
   // https://faculty.washington.edu/aragon/pubs/rst89.pdf
   //
   // s.ticket compared with zero in couple of places, therefore set lowest bit.
   // It will not affect treap's quality noticeably.
   s.ticket = fastrand() | 1
   s.parent = last
   *pt = s

   // Rotate up into tree according to ticket (priority).
   for s.parent != nil && s.parent.ticket > s.ticket {
      if s.parent.prev == s {
         root.rotateRight(s.parent)
      } else {
         if s.parent.next != s {
            panic("semaRoot queue")
         }
         root.rotateLeft(s.parent)
      }
   }
}

互斥锁解锁操作

// Unlock unlocks m.
// It is a run-time error if m is not locked on entry to Unlock.
//
// A locked Mutex is not associated with a particular goroutine.
// It is allowed for one goroutine to lock a Mutex and then
// arrange for another goroutine to unlock it.
func (m *Mutex) Unlock() {
   if race.Enabled {
      _ = m.state
      race.Release(unsafe.Pointer(m))
   }

   // Fast path: drop lock bit.
   // NOTE: Fast path模式,直接加上1 << 0,如果返回的新状态等于0,即当前 Goroutine 就成功解锁了互斥锁
   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.
      // 如果新状态不为0,意味着有其他等待的goroutine需要唤醒,开始慢速解锁
      m.unlockSlow(new)
   }
}

func (m *Mutex) unlockSlow(new int32) {
    // NOTE: 先校验锁状态的合法性 — 如果当前互斥锁已经被解锁过了会直接抛出异常 “sync: unlock of unlocked mutex” 中止当前程序
   if (new+mutexLocked)&mutexLocked == 0 {
      throw("sync: unlock of unlocked mutex")
   }
   
   // NOTE: 正常模式
   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.
         // NOTE: 如果没有等待的gorotine 或者 当前已经有唤醒的goroutine,不操作直接返回
         if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
            return
         }
         
         // Grab the right to wake someone.
         // NOTE:减少等待gouroutine数量,同时唤醒一个处于阻塞状态的goroutine
         new = (old - 1<<mutexWaiterShift) | mutexWoken
         if atomic.CompareAndSwapInt32(&m.state, old, new) {
            runtime_Semrelease(&m.sema, false, 1)
            return
         }
         old = m.state
      }
   } else {
      // NOTE: 饥饿模式
      // Starving mode: handoff mutex ownership to the next waiter, and yield
      // our time slice so that the next waiter can start to run immediately.
      // Note: mutexLocked is not set, the waiter will set it after wakeup.
      // But mutex is still considered locked if mutexStarving is set,
      // so new coming goroutines won't acquire it.
      // NOTE: 饥饿模式下,唤醒等待队列头的waiter, 并将当前锁交给它
      runtime_Semrelease(&m.sema, true, 1)
   }
}

// sync/runtime.go
// Semrelease atomically increments *s and notifies a waiting goroutine
// if one is blocked in Semacquire.
// It is intended as a simple wakeup primitive for use by the synchronization
// library and should not be used directly.
// If handoff is true, pass count directly to the first waiter.
// skipframes is the number of frames to omit during tracing, counting from
// runtime_Semrelease's caller.
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)

// runtime/sema.go
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
   semrelease1(addr, handoff, skipframes)
}

func semrelease1(addr *uint32, handoff bool, skipframes int) {
   root := semroot(addr)
   atomic.Xadd(addr, 1)

   // Easy case: no waiters?
   // This check must happen after the xadd, to avoid a missed wakeup
   // (see loop in semacquire).
   if atomic.Load(&root.nwait) == 0 {
      return
   }

   // Harder case: search for a waiter and wake it.
   lockWithRank(&root.lock, lockRankRoot)
   if atomic.Load(&root.nwait) == 0 {
      // The count is already consumed by another goroutine,
      // so no need to wake up another goroutine.
      unlock(&root.lock)
      return
   }
   // NOTE: 从队列头取一个处于阻塞状态的goroutine(*sudo)
   s, t0 := root.dequeue(addr)
   if s != nil {
      atomic.Xadd(&root.nwait, -1)
   }
   unlock(&root.lock)
   if s != nil { // May be slow or even yield, so unlock first
      acquiretime := s.acquiretime
      if acquiretime != 0 {
         mutexevent(t0-acquiretime, 3+skipframes)
      }
      if s.ticket != 0 {
         throw("corrupted semaphore ticket")
      }
      if handoff && cansemacquire(addr) {
         s.ticket = 1
      }
      // NOTE: 将goroutine唤醒,从阻塞状态转换到就绪态,等待调度
      readyWithTime(s, 5+skipframes)
      if s.ticket == 1 && getg().m.locks == 0 {
         // Direct G handoff
         // readyWithTime has added the waiter G as runnext in the
         // current P; we now call the scheduler so that we start running
         // the waiter G immediately.
         // Note that waiter inherits our time slice: this is desirable
         // to avoid having a highly contended semaphore hog the P
         // indefinitely. goyield is like Gosched, but it emits a
         // "preempted" trace event instead and, more importantly, puts
         // the current G on the local runq instead of the global one.
         // We only do this in the starving regime (handoff=true), as in
         // the non-starving case it is possible for a different waiter
         // to acquire the semaphore while we are yielding/scheduling,
         // and this would be wasteful. We wait instead to enter starving
         // regime, and then we start to do direct handoffs of ticket and
         // P.
         // See issue 33747 for discussion.
         // NOTE: handoff的作用是触发goyield, 即饥饿状态相比于普通状态,其goroutine被唤醒后放入的不是公共的就绪队列
         // 而是放在P的本地就绪队列中,减少不必要切换的开销
         goyield()
      }
   }
}

func readyWithTime(s *sudog, traceskip int) {
   if s.releasetime != 0 {
      s.releasetime = cputicks()
   }
   // NOTE: goready 转换goroutine状态
   goready(s.g, traceskip)
}

func goready(gp *g, traceskip int) {
   systemstack(func() {
      ready(gp, traceskip, true)
   })
}

// goyield is like Gosched, but it:
// - emits a GoPreempt trace event instead of a GoSched trace event
// - puts the current G on the runq of the current P instead of the globrunq
func goyield() {
   checkTimeouts()
   mcall(goyield_m)
}

参考

  1. https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-sync-primitives/#rwmutex
  2. https://blog.nowcoder.net/n/51b9f0ad8fd1474487aa59a7f6cc66b7
  3. http://legendtkl.com/2016/10/23/golang-mutex/
  4. https://zhuanlan.zhihu.com/p/350456432
  5. https://www.jianshu.com/p/79518c4b9bd2?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com
  6. https://blog.csdn.net/qcrao/article/details/116810643
  7. http://kmanong.top/kmn/qxw/form/article?id=73092&cate=93
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值