golang-互斥锁-mutex-源码阅读条记

[复制链接]
发表于 2025-10-10 01:28:34 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
互斥锁的机制

Go语言中互斥锁(Mutex)的公平性机制。互斥锁是一种用于掩护共享资源,防止多个goroutine同时访问的同步原语。这段表明具体分析白互斥锁的两种使用模式:平凡模式和饥饿模式。
平凡模式(Normal Mode)

在平凡模式下,等待获取锁的goroutine会按照先辈先出(FIFO)的序次列队。当一个等待的goroutine被唤醒时,它并不直接拥有锁,而是与新到达的goroutine竞争锁的全部权。新到达的goroutine由于已经在运行,因此在竞争中有上风。如果一个等待的goroutine在竞争中失败,它会被重新列队到等待队列的前面。如果一个goroutine一连1毫秒以上无法获取锁,互斥锁会切换到饥饿模式。
饥饿模式(Starvation Mode)

在饥饿模式下,锁的全部权直接从解锁的goroutine转达给等待队列前面的goroutine。新到达的goroutine纵然发现锁处于未锁定状态,也不会实验获取锁,而是直接列队到等待队列的末了。这种模式下,新到达的goroutine不会实验自旋(即不停实验获取锁)。
模式切换

如果一个goroutine乐成获取到锁,而且发现它是队列中的末了一个等待者,大概它等待的时间少于1毫秒,它会将互斥锁切换回平凡模式。
性能思量

平凡模式具有更好的性能,由于一个goroutine可以一连多次获取锁,纵然有壅闭的等待者。饥饿模式则用于防止非常情况下的尾部延长标题,确保等待时间较长的goroutine可以大概及时获取锁。
先容

数据结构
  1. // A Mutex is a mutual exclusion lock.
  2. type Mutex struct {
  3.         state int32
  4.         sema  uint32
  5. }
复制代码

包中的常量
  1. const (
  2.   // mutex = 1
  3.         mutexLocked = 1 << iota // mutex is locked
  4.   // mutexWoken = 2
  5.         mutexWoken
  6.   // mutexStarving = 4
  7.         mutexStarving
  8.   // mutexWaiterShift = 3
  9.         mutexWaiterShift = iota
  10.         // Mutex fairness.
  11.         //
  12.         // Mutex can be in 2 modes of operations: normal and starvation.
  13.         // In normal mode waiters are queued in FIFO order, but a woken up waiter
  14.         // does not own the mutex and competes with new arriving goroutines over
  15.         // the ownership. New arriving goroutines have an advantage -- they are
  16.         // already running on CPU and there can be lots of them, so a woken up
  17.         // waiter has good chances of losing. In such case it is queued at front
  18.         // of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
  19.         // it switches mutex to the starvation mode.
  20.         //
  21.         // In starvation mode ownership of the mutex is directly handed off from
  22.         // the unlocking goroutine to the waiter at the front of the queue.
  23.         // New arriving goroutines don't try to acquire the mutex even if it appears
  24.         // to be unlocked, and don't try to spin. Instead they queue themselves at
  25.         // the tail of the wait queue.
  26.         //
  27.         // If a waiter receives ownership of the mutex and sees that either
  28.         // (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
  29.         // it switches mutex back to normal operation mode.
  30.         //
  31.         // Normal mode has considerably better performance as a goroutine can acquire
  32.         // a mutex several times in a row even if there are blocked waiters.
  33.         // Starvation mode is important to prevent pathological cases of tail latency.
  34.   
  35.   // 100000
  36.         starvationThresholdNs = 1e6
  37. )
复制代码
加锁
  1. func (m *Mutex) Lock() {
  2.         // Fast path: grab unlocked mutex.
  3.         if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
  4.                 return
  5.         }
  6.         // Slow path (outlined so that the fast path can be inlined)
  7.         m.lockSlow()
  8. }
复制代码
fast path

如果state为0,也就是没有被加锁,也不是饥饿模式,没有列队等待的goroutine。直接走fast path,通过原子使用给mutex加上锁
如果失败,则进入slow path
slow path

自旋

满足一下三种情况进入自旋模式

  • mutex被加锁,也就是mutexLocked标志位为1
  • mutex不是饥饿模式
  • 满足自旋的条件,如下

进入自旋后,如果无协程正在取锁,但是有协程在壅闭等待,则将mutexWoken标志位置为1
  1. var waitStartTime int64
  2.         starving := false
  3.         awoke := false
  4.         iter := 0
  5.         old := m.state
  6.         for {
  7.                 // Don't spin in starvation mode, ownership is handed off to waiters
  8.                 // so we won't be able to acquire the mutex anyway.
  9.                 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  10.       // mutex已被加锁且不是饥饿模式,并且runtime_canSpin()返回true
  11.       
  12.                         // Active spinning makes sense.
  13.                         // Try to set mutexWoken flag to inform Unlock
  14.                         // to not wake other blocked goroutines.
  15.       // 如果无协程正在取锁,但是有协程在阻塞等待,则将mutexWoken标志位置为1
  16.                         if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
  17.                                 atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
  18.         awoke = true
  19.                         }
  20.                         runtime_doSpin()
  21.                         iter++
  22.                         old = m.state
  23.                         continue
  24.                 }
复制代码
根据状态位设置新的状态位

不满足自旋情况后,也就是三个条件不满足之后进入这一步

  • mutex的mutexLocked标志位未被加锁
  • mutex处于饥饿模式
  • 不满足自旋条件
会不绝举行下面三步

  • 如果不是饥饿模式,设置加锁标志位,实验抢锁。饥饿模式的话,需列队抢锁
  • 如果状态已被加锁大概饥饿模式,增长state中等待的goroutine数目
  • 如果进入饥饿模式,且已被加锁,则将新值设置为饥饿模式
  • 如果已经有壅闭的协程被唤醒,须要将mutexWoken标志位置为0;由于接下来要么是加锁乐成,要么失败壅闭
  1. for {
  2.                 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
  3.                         // ... 自旋逻辑
  4.                 }
  5.                 new := old
  6.                 // Don't try to acquire starving mutex, new arriving goroutines must queue.
  7.     // 不处于饥饿模式,考虑抢锁
  8.                 if old&mutexStarving == 0 {
  9.                         new |= mutexLocked
  10.                 }
  11.     // 如果被加锁或者饥饿模式,增加state中等待的goroutine数量
  12.                 if old&(mutexLocked|mutexStarving) != 0 {
  13.                         new += 1 << mutexWaiterShift
  14.                 }
  15.                 // The current goroutine switches mutex to starvation mode.
  16.                 // But if the mutex is currently unlocked, don't do the switch.
  17.                 // Unlock expects that starving mutex has waiters, which will not
  18.                 // be true in this case.
  19.     // 如果进入饥饿模式,且已被加锁,则将新值设置为饥饿模式
  20.                 if starving && old&mutexLocked != 0 {
  21.                         new |= mutexStarving
  22.                 }
  23.     // 如果已经有阻塞的协程被唤醒,需要将mutexWoken标志位置为0
  24.     // 因为接下来要么是加锁成功,要么失败阻塞
  25.                 if awoke {
  26.                         // The goroutine has been woken from sleep,
  27.                         // so we need to reset the flag in either case.
  28.                         // ...
  29.                         new &^= mutexWoken
  30.                 }
复制代码
更换新值

抢锁


  • CAS失败,分析mutex的值已被修改,进入新一轮加锁流程
  • CAS乐成,旧值未加锁且不是饥饿模式,加锁乐成,退出循环
  • 未抢到锁大概饥饿模式,如果是第一次壅闭,设置等待起始时间
  • 壅闭挂起, 老协程插入队头,新协程插入队尾
唤醒


  • 判定是否是饥饿模式
  • 如果是饥饿模式,无需竞争加锁,直接乐成。更新状态值
  • 如果不是饥饿模式,进入新一轮加锁流程
  1. // ... 自旋
  2. // ... 根据状态位设置新值
  3.     if atomic.CompareAndSwapInt32(&m.state, old, new) {
  4.       // 加锁成功且不是饥饿模式,直接退出
  5.                         if old&(mutexLocked|mutexStarving) == 0 {
  6.                                 break // locked the mutex with CAS
  7.                         }
  8.                         // If we were already waiting before, queue at the front of the queue.
  9.                         queueLifo := waitStartTime != 0
  10.       // 新进入抢锁的协程,未阻塞过,设置等待起始时间
  11.                         if waitStartTime == 0 {
  12.                                 waitStartTime = runtime_nanotime()
  13.                         }
  14.       // 阻塞, 老协程插入队头,新协程插入队尾
  15.                         runtime_SemacquireMutex(&m.sema, queueLifo, 2)
  16.       // 被唤醒,设置饥饿模式,原来是饥饿模式或者等待时间超过阈值1ms
  17.                         starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
  18.                         old = m.state
  19.       // 饥饿模式被唤醒,无需竞争抢锁
  20.                         if old&mutexStarving != 0 {
  21.                                 // If this goroutine was woken and mutex is in starvation mode,
  22.                                 // ownership was handed off to us but mutex is in somewhat
  23.                                 // inconsistent state: mutexLocked is not set and we are still
  24.                                 // accounted as waiter. Fix that.
  25.                                 // ... 检测状态是否正常
  26.         // 更新锁的状态,等待的协程个数,是否为饥饿模式
  27.                                 delta := int32(mutexLocked - 1<<mutexWaiterShift)
  28.                                 if !starving || old>>mutexWaiterShift == 1 {
  29.                                         // Exit starvation mode.
  30.                                         // Critical to do it here and consider wait time.
  31.                                         // Starvation mode is so inefficient, that two goroutines
  32.                                         // can go lock-step infinitely once they switch mutex
  33.                                         // to starvation mode.
  34.                                         delta -= mutexStarving
  35.                                 }
  36.                                 atomic.AddInt32(&m.state, delta)
  37.                                 break
  38.                         }
  39.       // 如果不是饥饿模式,进入新的一轮抢锁流程
  40.                         awoke = true
  41.                         iter = 0
  42.                 } else {
  43.       // CAS失败,重新进入循环
  44.                         old = m.state
  45.                 }
复制代码
解锁

快速路径

如果发现只有一个goroutine等待解锁,直接返回即可
如果发现锁中另有壅闭协程,则走入unlockSlow分支
  1. func (m *Mutex) Unlock() {
  2.         // Fast path: drop lock bit.
  3.         new := atomic.AddInt32(&m.state, -mutexLocked)
  4.         if new != 0 {
  5.                 // Outlined slow path to allow inlining the fast path.
  6.                 // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
  7.                 m.unlockSlow(new)
  8.         }
  9. }
复制代码
慢分支

未被加锁

倘若发现mutex此前未被加锁,直接抛出fatal
  1. func (m *Mutex) unlockSlow(new int32) {
  2.         if (new+mutexLocked)&mutexLocked == 0 {
  3.                 fatal("sync: unlock of unlocked mutex")
  4.         }
  5.         ...
  6. }
复制代码
非饥饿模式

如果不是饥饿模式
且无等待的壅闭协程,大概mutexLocked|mutexWoken|mutexStarving任一标志位不为0,分析已经有其他活泼协程到场,无需关心后续流程
如果不满足,则唤醒一个等待的协程,失败则再次进入循环
  1. if new&mutexStarving == 0 {
  2.     old := new
  3.     for {
  4.         // If there are no waiters or a goroutine has already
  5.         // been woken or grabbed the lock, no need to wake anyone.
  6.         // In starvation mode ownership is directly handed off from unlocking
  7.         // goroutine to the next waiter. We are not part of this chain,
  8.         // since we did not observe mutexStarving when we unlocked the mutex above.
  9.         // So get off the way.
  10.         if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
  11.             return
  12.         }
  13.         // Grab the right to wake someone.
  14.         new = (old - 1<<mutexWaiterShift) | mutexWoken
  15.         if atomic.CompareAndSwapInt32(&m.state, old, new) {
  16.             runtime_Semrelease(&m.sema, false, 2)
  17.             return
  18.         }
  19.         old = m.state
  20.     }
  21. }
复制代码
饥饿模式

直接唤醒壅闭协程队列头部的协程
  1. if new&mutexStarving == 0 {
  2.         ... // 非饥饿模式的操作
  3.         } else {
  4.                 // Starving mode: handoff mutex ownership to the next waiter, and yield
  5.                 // our time slice so that the next waiter can start to run immediately.
  6.                 // Note: mutexLocked is not set, the waiter will set it after wakeup.
  7.                 // But mutex is still considered locked if mutexStarving is set,
  8.                 // so new coming goroutines won't acquire it.
  9.                 runtime_Semrelease(&m.sema, true, 2)
  10.         }
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表