八股文

Go互斥锁加锁逻辑中的的两种运行模式


提前给结论

一开始,互斥锁是处于正常模式。 在加锁的时候,如果没有成功,则开始自旋。 如果自旋4次后还没有拿到锁,则改协程会被加入到一个FIFO队列中,并阻塞等待唤醒 正常模式下,被唤醒的协程不会直接拥有锁,而是会和新请求锁的协程竞争锁。 饥饿模式下,直接获得锁,不用和新的协程竞争锁。

Go中的互斥锁

Go中互斥锁,就是一个sync.Mutex类型。其特点是只能存在一个写者或读者,不能同时读和写。

sync.Mutex代码位于sync/mutex.go中

结构体定义

type Mutex struct {
	state int32
	sema  uint32
}

其中state标识状态,sema是信号量。

state第一个bit标识是否锁定,mutexLocked

第二个bit位标识是否被协程唤醒,mutexWoken

第三个bit位标识是否是饥饿模式,mutexStarving

mutex.drawio

上锁

先看一下上锁的逻辑:

func (m *Mutex) Lock() {
	// 对state 进行原子操作 本质是加锁
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// 如果加锁不成功 调用lockSlow
	m.lockSlow()
}

在lockSlow方法中

// 如果已经被锁定 但是不是饥饿模式 并且可以自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
    //***
    runtime_doSpin() // 自旋等待获取锁
    //***
}
// starvationThresholdNs就是1ms 如果超过1ms没有获取到锁,则进入饥饿模式
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs

综上,加锁的流程如下:

mutex.lock

是否能自选的判断:

func sync_runtime_canSpin(i int) bool {
	if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
		return false
	}
	if p := getg().m.p.ptr(); !runqempty(p) {
		return false
	}
	return true
}

通过代码我们可以分析出,能自旋的条件有:

解锁

再看一下解锁的逻辑

func (m *Mutex) Unlock() {
    //先尝试直接解锁
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// 如果有别的协程等待 需要唤醒等待的协程
		m.unlockSlow(new)
	}
}

unlockSlow的实现

func (m *Mutex) unlockSlow(new int32) {
    // ***
    // 如果是饥饿模式
	if new&mutexStarving == 0 {
		old := new
		for {
            // 如果没有需要唤醒的协程 或者一个协程已经拿到锁 则不进行解锁
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
            // 解锁 然后唤醒别的协程
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
        // 如果不是饥饿模式 直接唤醒阻塞的协程
		runtime_Semrelease(&m.sema, true, 1)
	}
}

综上,解锁的流程如下:

mutex.unlock

同时需要注意: