sync.mutex 源码分析

1,095 阅读8分钟

sync.mutex 是 goroutine 粒度的互斥锁, 关于 goroutine 相关的挂起和唤醒是调用 sema.go 中的方法来实现的,而sema.go 中关于 m 的加锁和唤醒则是在 lock_sema 中实现的。

sync.mutex

// 在首次使用之后 Mutex 不能被复制
// 为啥这么说呢,因为值复制之后新的 Mutex 变量会有新的地址,同样作为 Mutex 的属性 sema 对应的
// 指针地址也是变成新的,而 sema 的地址是获取等待队列的关键参数,so。。。
type Mutex struct {
	state int32 // 状态位
	sema  uint32 // sema的指针作为semacquire1的参数
}

Mutex的状态字段 state 不同的位代表不同的状态以及等待者的数量

  • mutexLocked 锁定状态,多个 goroutine 竞争,只要有 g 获取到锁,就会把锁状态位(Mutex.state)设定为该状态
  • mutexWoken 唤醒状态,当 g 进行解锁操作时, 如果当前锁不是饥饿状态并且有其他等待的 g,就会把锁状态位设成唤醒状态,为了让新来的任务不至于在抢锁的第一道 cas 就成功,让新来的 g 和唤醒的 g 在 lockSlow函数抢一抢
  • mutexStarving 饥饿状态, 当 g 从锁定到被唤醒超过一毫秒,并且被唤醒之后还是没抢到锁,这个时候就会把就会把饥饿状态合并到锁的状态位中,饥饿状态的 g 被唤醒之后立即被 m 从 p 中取出来执行
  • mutexWaiterShift 等待者数 的状态位,新增等待者者时新状态位 new += 1 << mutexWaiterShift

lock

g 首先会 cas 抢锁,抢到就直接返回,相当于进入到锁中。抢不到锁,就进入到 lockSlow 中。

在 lockSlow 的过程中,会先判断锁的状态,假如当前锁不处于饥饿状态但是处于锁定状态,就会判断 g 是否自旋。假如可以自旋,并且当前锁不处于唤醒状态,就设置锁状态为唤醒,并且自旋。之所以多出设置唤醒状态,是因为在解锁的过程中假如发现锁又处在唤醒状态,就不去唤醒队列中的 g,直接释放锁。也就是说,假如有 g 竞争锁但是还没有进入到等待队列(比如当前处在自旋中的 g),就不需要唤醒队列中的 g,直接把锁让给这些 g 去竞争执行。

不管当前是处于锁定还是饥饿状态,都会把等待者数量增加,然后再次去 cas 设置状态位抢锁。假如老的状态位既不是锁定也不是饥饿状态,说明抢到锁了。否则就会进入队列等待。

假如被唤醒的时间小于一毫秒或者当前 g 是最后一个,并且当前锁是饥饿状态,就会取消饥饿状态。

当被唤醒的时间大于一毫秒,此时不会直接把锁状态设置为饥饿,只有锁当前是锁定状态,才会设置为饥饿状态,AKA,当前有其他的 g 获取到锁了。也就是说假如被唤醒的时间超过一毫秒但是立即获取到锁了,就不会变成饥饿状态。

lock 源码

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	// cas 抢占锁, 抢到了就返回
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

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.
		// 饥饿模式不能自旋,所有权转交给等待的 goroutine, 所以不能获取到锁
		// 处在锁定状态,但不是饥饿状态,并且可以自旋
		// 此处为啥要自旋呢,当然是为了减少park和unpark的切换的开销,假如自旋等一会就能获取到锁,那就没必要阻塞了
		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.
			// 不是唤醒状态,并且队列不为0,设置为唤醒状态
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			// 自旋
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		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.
		// 如果当前不是锁定状态,就不要切换到饥饿状态
		// 只有当前goroutine 等待超过一毫秒并且当前是锁定状态,aka 当前有其他 goroutine 在当前 goroutine 之前获取到锁
		// 也就是说,加入当前 goroutine 等待超过一毫秒,但是立马获取到锁了,就不用设置成饥饿模式
		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
		}
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// 既不是锁定也不是饥饿,跳出
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			// If we were already waiting before, queue at the front of the queue.
			// 如果之前已经等过了,直接插到队列的头部
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// 之前没等过,插到尾部,并且park
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			// 超过一毫秒没被唤醒
			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.
				// 饥饿状态是直接解除锁定并唤醒第一个等待的 goroutine ,锁的状态不会是锁定和唤醒,而且肯定存在等待队列
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// 等待队列数减一
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				// 如果等待时间小于一毫秒,或者是最后一个 goroutine ,解除饥饿状态
				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.
					// 饥饿模式非常低效,一旦两个 goroutine 都切换锁到饥饿状态, 可能会无限制的锁定
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}

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

unlock

首先取消锁定状态,假如当前有等待者就会进入 unlockSlow

unlockSlow 会先判断是否是饥饿状态,假如是饥饿状态,把等待队列中的sudog 对应的 g 变成待执行状态,并且放到当前 m 关联的 p 的队列的下一个执行的位置, 并且有可能会把当前 m 执行的 g 放到 p 本地队列的队尾,然后立刻执行上面的 g,减等待着值的操作在唤醒之后操作,避免多耗时

不是饥饿状态,判断队列是否有等待着和当前锁的状态,假如有等待者并且不是唤醒、锁定、饥饿状态,就减少等待者数量,并且设置当前为唤醒状态,设置成已唤醒状态,而不是设置成0,为了新来的任务不至于在抢锁的第一道cas就成功,让新来的任务和唤醒的任务在 lockSlow 里抢一抢。最后把等待队列中的sudog 对应的 g 变成待执行状态,并且放到当前 m 关联的 p 的队列的下一个执行的位置,等待 m 执行

unlock 源码

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.
			// 没有等待的队列或者又处在三个状态之一
			// 处在这三个状态之一说明已经又在竞争锁了,而且没进入到队列中,这个时候就不需要唤醒队列里的 g了
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// Grab the right to wake someone.
			// 减少等待队列的值,并把低位设置为已唤醒状态
			// 设置成已唤醒状态,而不是设置成0,为了新来的任务不至于在抢锁的第一道cas就成功,让新来的任务和唤醒的任务在 lockSlow 里抢一抢
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				// 把等待队列中的sudog 对应的 g 变成待执行状态,并且放到当前 m 关联的 p 的队列的下一个执行的位置,等待 m 执行
				runtime_Semrelease(&m.sema, false, 1)
				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.
		// 把等待队列中的sudog 对应的 g 变成待执行状态,并且放到当前 m 关联的 p 的队列的下一个执行的位置
		// 并且有可能会把当前 m 执行的 g 放到 p 本地队列的队尾,然后立刻执行上面的 g
		runtime_Semrelease(&m.sema, true, 1)
	}
}