sync.Mutex

66 阅读8分钟

结构体定义

type Mutex struct {
	state int32 // 互斥锁的状态
	sema  uint32 // 控制锁状态的信号量
}

互斥锁的状态

state字段使用不同位表示互斥锁的不同状态

  • 第0位表示是否已被锁定:mutexLocked
  • 第1位表示是否已被唤醒:mutexWoken
  • 第2位表示是否处于饥饿模式:mutexStarving
  • 第3位到第31位表示当前等待的数量
graph TB
    subgraph "互斥锁state位图"
        direction TB
        
        A["<b>waitersCount (29位)</b><br/>存储等待获取锁的 goroutine 数量<br/>"]
        B["<b>starving (1位)</b><br/>饥饿模式标志<br/>0: 正常模式 | 1: 饥饿模式 mutexStarving "]
        C["<b>woken (1位)</b><br/>唤醒标志<br/>0: 无唤醒 | 1: 有 goroutine 被唤醒 mutexWoken "]
        D["<b>locked (1位)</b><br/>锁定标志<br/>0: 未锁定 | 1: 已锁定 mutexLocked "]
        
        style A fill:#7dd3c0,stroke:#333,stroke-width:2px,color:#000
        style B fill:#ffd93d,stroke:#333,stroke-width:2px,color:#000
        style C fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#000
        style D fill:#4a5568,stroke:#333,stroke-width:2px,color:#fff
    end

正常模式和饥饿模式

互斥锁的状态分为两种模式:正常模式和饥饿模式

正常模式

  • 互斥锁的等待者按照FIFO顺序排队获取锁
  • 被唤醒的等待者并不直接拥有互斥锁,需要与新到达的goroutine竞争互斥锁的所有权,被唤醒的等待者大概率不会获取到锁
    • 新到达的goroutine的优势
      • 已经在CPU上运行(无需切换上下文)
      • 数量可能较多,竞争激烈(因为唤醒的Goroutine只有一个,但是处于自旋状态下的会有多个Goroutine)
    • 等待者的劣势
      • 需要唤醒
      • 可能需要切换上下文

饥饿模式

  • 进入条件:当Goroutine超过1ms没有获取到锁,就会进入饥饿模式
  • 工作流程
    • 互斥锁将所有权直接交给等待队列中的首个Goroutine
    • 新到的Goroutine不会尝试获取锁,而是进入等待队列的末尾
  • 退出条件(两个条件满足任意条件即可退出):
    • 获取互斥锁所有权的Goroutine处于等待队列的末尾
    • Goroutine的等待时间小于1ms

加锁流程

加锁分为快速流程和慢速流程两种

  • Fast Path: 直接执行CAS,让当前的Goroutine获取锁的所有权
    • 只有在state为0的情况下,会执行成功
    • CAS失败,则转入slowpath
  • Slow Path: 会经历自旋,计算state,更新state并获取锁三个阶段
func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		...
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

Slow Path

自旋

  • 在Slow Path流程下,Mutex会尝试进行自旋来等待锁被释放或切换到饥饿模式
    • 自旋的条件
      • Mutex状态条件
        • Mutex处于正常模式并且处于加锁状态
      • canSpin条件
        • 已经自旋的次数<4次
        • 所在的机器是多核的
          • 单核自旋没意义,并且会造成死锁,想象下:G1持有锁,G2自旋一直占有CPU,G1的锁不会被释放
        • 除了当前的处理器P,还有一个空闲的处理器P(同理单核)
        • 当前的处理器P上的运行队列是空的
    • 可以自旋的Goroutine,会尝试设置唤醒标志位,阻止Unlock唤醒其他Goroutine
      • 条件
        • 唤醒标志位未设置过
        • 存在等待获取锁的Gorutine
    • 结束自旋的条件
      • 达到自旋的最大次数限制
      • 互斥锁所有权已被释放
      • 互斥锁从正常模式切换到饥饿模式
    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.
    		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 && // 抢占唤醒标志位,避免被Unlock唤醒其他Goroutine
    				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
    				awoke = true
    			}
    			runtime_doSpin() // 执行PAUSE
    			iter++
    			old = m.state
    			continue
    		}
    		...
    	}
    }
    

计算状态

  • 如果当前的状态未处于饥饿模式,则设置新的状态的锁定标志位为已锁定
  • 如果当前互斥锁的状态为已加锁|处于饥饿模式,则新状态中的等待数量+1
  • 如果处于饥饿模式,并且当前互斥锁的状态不是未加锁的状态,则设置新状态饥饿模式标志位(因为Unlock期待饥饿模式下的互斥锁有等待队列)
  • 如果当前Goroutine已经被唤醒,则需要清除唤醒标志位(抢锁成功后,Unlock会根据这个来唤醒其他锁)
		new := old
		// Don't try to acquire starving mutex, new arriving goroutines must queue.
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		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.
		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
		}

更新状态并获取锁

通过CAS操作将步骤二中计算的状态写入到state中

  • 存在写入失败的情况,当其他Goroutine更新并抢占锁成功后,需要重新从自旋流程开始
    • 写入成功后
      • 判断更新前的状态是否是未解锁状态
        • 未解锁状态证明已经成功获取锁
      • 判断当前Goroutine是否已经排队过,加入等待队列,通过信号量使当前Goroutine休眠,等待被唤醒
        • 已经排过队,加入到等待队列的队头
        • 没有排过队,加入到等待队列的队尾
      • 唤醒后,根据已经等待锁的时间判断(等待时间>1ms)以及当前是否处于饥饿模式,判断是否需要进入到饥饿模式
        • 如果处于饥饿状态(证明此时是排队唤醒的,此时直接加锁)
          • 需要判断是否需要退出饥饿状态
          • 加锁
          • 等待者数量-1
        • 未处于饥饿模式,则继续执行自旋流程
    • 写入失败(证明已经有其他人更新了状态了,需要从头开始执行)
      • 继续从自旋流程执行
      		if atomic.CompareAndSwapInt32(&m.state, old, new) { // 将计算好的状态通过CAS写入
      			if old&(mutexLocked|mutexStarving) == 0 { // 如果当前的状态是未加锁的状态,则new计算后的结果一定是加锁了,直接break结束流程
      				break // locked the mutex with CAS
      			}
      			// If we were already waiting before, queue at the front of the queue.
      			queueLifo := waitStartTime != 0 // queueLifo 如果为true,则加入队头,如果为false,则加入队尾
      			if waitStartTime == 0 {
      				waitStartTime = runtime_nanotime()
      			}
      			runtime_SemacquireMutex(&m.sema, queueLifo, 2) // 加入等待队列后,休眠
      			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs // 计算是否应该切换到饥饿状态
      			old = m.state
      			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   // 如果未处于饥饿状态,证明获取锁的时间<1ms, 或当前Goroutine如果是等待者队列的最后一位,则也退出饥饿模式
      				}
      				atomic.AddInt32(&m.state, delta) // 加锁并对等待者数量-1
      				break
      			}
      			awoke = true 
      			iter = 0 // 重置自旋数量,重新开始自旋
      		} else {
      			old = m.state
      		}
      

解锁流程

解锁同加锁一样,都分为快速和慢速两种

Fast Path: 通过AddInt32直接对互斥锁快速解锁

  • 如果AddInt32返回的新的状态为0,则证明已经解锁,并且没有任何的等待者,以及不处于饥饿模式
  • 如果新的状态不为0,则需要通过Slow Path唤醒其他等待的Goroutine
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) // 唤醒其他Goroutine
	}
}

Slow Path

  • Slow Path首先会判断当前互斥锁是否处于饥饿模式
    • 未处于饥饿模式下会循环直到唤醒一个新的Goroutine:
      • 首先需要判断是否需要唤醒:当等待队列中没有等待的Goroutine后,或者已经有其他Goroutine已经处于加锁|被唤醒的状态下,或者已经处于饥饿模式下了,则同样不需要唤醒
      • 计算新的状态,等待者数量-1并且唤醒位设置为已唤醒
      • 更新状态并唤醒其他Goroutine
        • 通过CAS更新互斥锁的状态为计算后的状态,更新成功后,会唤醒等待队列中的Goroutine
        • 如果更新失败,则需要从判断唤醒处重新执行
    • 处于饥饿状态下
      • 唤醒队列中的第一个等待者Goroutine
func (m *Mutex) unlockSlow(new int32) {
	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<<mutexWaiterShift) | mutexWoken // 计算状态
			if atomic.CompareAndSwapInt32(&m.state, old, new) { // 更新状态
				runtime_Semrelease(&m.sema, false, 2) // 更新成功后唤醒,但此时需要与自旋的争抢锁的所有权
				return
			}
			old = m.state
		}
	} else {
		// 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.
		runtime_Semrelease(&m.sema, true, 2) // 饥饿模式下,直接唤醒第一个等待者
	}
}