go mutex源码分析与属性详解

445 阅读4分钟

Mutex数据结构

type Mutex struct {
	state int32
	sema  uint32
}

其中state字段会包含四个字段意思

在默认情况下,互斥锁的所有状态位都是 0,int32 中的不同位分别表示了不同的状态:

  1. mutexLocked — 表示互斥锁的锁定状态;
  2. mutexWoken — 表示当前正在有Goroutine在竞争锁,后面会进行详解;
  3. mutexStarving — 当前的互斥锁进入饥饿状态;
  4. waitersCount — 当前互斥锁上等待的 Goroutine 个数;

1. 锁的获取

首先看下lock方法的逻辑,源码sync/mutex.go

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	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()
}

通过cas方式尝试获取锁,成功后立马返回,若通过cas获取失败了,则使用locksSlow方法获取 locksSlow方法主要会经过以下逻辑:

  1. 先判断当前Goroutine是否可以满足自旋条件,若满足则会进行4次自旋
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.
   //首次自旋时,设置mutexWoken,说明当前正在有Goroutine尝试获取锁,这样在释放锁的时候,在正常模式下就不会唤醒阻塞队列里的Goroutine
   if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
      atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
      awoke = true
   }
   //具体自旋方法,让cpu空转
   runtime_doSpin()
   iter++
   old = m.state
   continue
}
  1. 设置state中新的状态值
		new := old
		// Don't try to acquire starving mutex, new arriving goroutines must queue.
		//如果当前state状态不是饥饿模式,则把mutexLocked标志位设置为1
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		//如果当前state中,已经被锁定或处于饥饿模式,则把等待数锁的个数加一
		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.
		//如果当前获取锁处于饥饿模式,则把mutexStarving标志位设置为1
		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
		}
  1. 设置好state新的值后,然后尝试再次通过cas方式获取锁
//尝试通过cas方式获取锁
if atomic.CompareAndSwapInt32(&m.state, old, new) {
            //判断通过cas获取锁是否成功,成功则立马返回,因为new中statemutexLocked|mutexStarving这两个标志位肯定有一个是为1的,假如为0,说明old state中,这两个初始值都为0,所以可以认为cas获取成功
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			//如果cas获取锁没有成功,则走以下流程
			// If we were already waiting before, queue at the front of the queue.
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			//runtime_SemacquireMutex会将当前Goroutine加入到阻塞队列中,同时触发当前m重新调度选取Goroutine执行
			//阻塞的
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			//当Goroutine被重新获得m执行时,会判断Goroutine阻塞等待了多长时间,
			//若大于某个阈值,说明阻塞队列中可能存在较多待执行的Goroutine,所以此时要把本地starving变量设置为true,让前面阻塞的Goroutine得到执行的机会
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if old&mutexStarving != 0 {
			//如果Goroutine被唤醒后,发现当前是饥饿模式,则当前Goroutine直接获取锁
				// 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")
				}
				//设置mutexLocked标志位
				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
		}

这里面逻辑细节其实比较多:

  1. 首先通过cas方式尝试获取锁,获取到则里面返回,否则继续执行
  2. 执行runtime_SemacquireMutex方法,该方法会将当前Goroutine加入到等待队列中,同时重新触发Goroutine的调度
  3. 当当前Goroutine被唤醒后,会继续执行下面代码,这个时候会判断Goroutine的等待时间,若超过阈值,则会将本地变量starving设置为true
  4. 被唤醒的Goroutine发现当前锁已经是饥饿模式时,表示当前Goroutine可以立马获取到锁,不需要再进行竞争获取锁
  5. 如果被唤醒的Goroutine发现当前state不是饥饿模式,则会重新参与锁的竞争

2. 锁的释放

锁的释放代码则相对简单,先看下代码

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// 修改锁的锁定状态
	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.
		//尝试着唤醒等待队列中的一个Goroutine,
		m.unlockSlow(new)
	}

从上面代码可以看出,锁状态先于Goroutine的唤醒,所以会存在一个问题,刚进来的Goroutine由于锁状态的改变可以立马抢占锁,而等待队列中的Goroutine还在被唤醒中,此时已经慢了一拍了。

在unlockSlow中释放锁主要有两个细节逻辑:

  1. 假如当前处于饥饿模式,则直接唤醒等待队列中一个Goroutine
  2. 假如当前处于正常模式且mutexWoken为true,说明已经有Goroutine在自旋获取锁,此时不需要唤醒等待队列中的Goroutine

其中mutexWoken重点介绍下,mutexWoken若为true可以看到在/sync/mutex.go中lockSlow方法中:

  1. 表示当前有Goroutine正在通过自旋方式获取锁
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
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
}

表示当前有Goroutine正在通过自旋的方式获取锁,设置mutexWoken标志后,在正常模式下,在unlock方法中就不会从等待锁的阻塞队列中唤醒一个Goroutine,这样保证当前自旋的Goroutine有更大的概率获取到锁

参考:

  1. draveness.me/golang/docs…