go语言之Mutex

mutex工作机制

Mutex有两种工作模式:正常模式和饥饿模式
在正常模式中,等待着按照FIFO的顺序排队获取锁,但是一个被唤醒的等待者有时候并不能获取mutex,它还需要和新到来的goroutine们竞争mutex的使用权。新到来的goroutine有一个优势,因为新到达的goroutine已经在CPU上运行了,因此一个被唤醒的等待者有很大的概率获取不到锁。在这种情况下它处在等待队列的前面。如果一个goroutine等待mutex释放的时间超过1ms,它就会将mutex切换到饥饿模式。

在饥饿模式中,mutex的所有权直接从解锁的goroutine递交到等待队列中排在最前方的goroutine。新到达的goroutine们不要尝试去获取mutex,即便它看起来是解锁状态,也不要尝试自旋,而是排到等待队列的尾部。

在饥饿模式下,有一个goroutine获取到mutex锁了,如果它满足下条件中的任意一个,mutex将会切换回去正常模式:

  1. 等待队列只有一个goroutine
  2. goroutine的等待时间小于1ms

正常模式有更好的性能,因为goroutine可以连续多次获得mutex锁,以及避免多个线程的排队消耗。
饥饿模式需要预防队列尾部goroutine一致无法获取mutex锁的问题。

mutex数据结构以及调用的函数

type Mutex struct {
    state int32 //将一个32位整数拆分为:从最高位排列
    //当前阻塞的goroutine数(29位)
    //饥饿状态(1位)
    //唤醒状态(1位)
    //锁状态(1位) 
    sema uint32 // 信号量
}

const (
    mutexLocked = 1 << iota // 用最后一位表示当前锁的状态,0-未锁住 1-已锁住
    mutexWoken // 用倒数第二位表示当前锁是否被唤醒 0-唤醒 1-未唤醒
    mutexStarving // 用倒数第三位表示当前锁是否为饥饿模式,0为正常模式,1为饥饿模式。
    mutexWaiterShift = iota // 3,从倒数第四位往前的bit位表示在排队等待的goroutine数
    starvationThresholdNs = 1e6 // 1ms
)
  1. runtime_canSpin。判断是否需要自选,golang中自旋锁并不会一直自旋下去,在runtime包中runtime_canSpin方法做了一些限制, 传递过来的iter大等于4或者cpu核数小等于1,最大逻辑处理器大于1(多核),至少有个本地的P队列,并且本地的P队列可运行G队列为空才会进行自旋。
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
}
  1. runtime_doSpin。 会调用procyield函数,该函数也是汇编语言实现。函数内部[循环]调用PAUSE指令。PAUSE指令什么都不做,但是会消耗CPU时间,在执行PAUSE指令时,CPU不会对它做不必要的优化。
func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}
  1. runtime_SemacquireMutex。 一个gotoutine的等待队列,如果lifo为true,则插入队列头,否则插入队尾
func runtime_SemacquireMutex(s *uint32, lifo bool)
  1. runtime_Semrelease。唤醒被runtime_SemacquireMutex函数挂起的等待goroutine,如果handoff为true,唤醒队列头第一个等待者,否则的话可能是随机
func runtime_Semrelease(s *uint32, handoff bool)

Lock方法实现

Lock方法申请对mutex加锁,Lock执行的时候,分三种情况:

  1. 无冲突。通过CAS操作把当前状态设置为加锁状态。
  2. 有冲突。开始runtime_canSpin自旋,并等待锁释放,如果其他goroutine在这段时间内释放了该锁,直接获得该锁;如果没有释放进入第3步。
  3. 有冲突,且已经过了自旋阶段。通过调用seamacquire函数来让当前goroutine进入等待状态。
// 无冲突
// CompareAndSwapInt32相等才赋值,且返回true
// 查看 state 是否为0(空闲状态), 如果是则表示可以加锁,将其锁置为锁定状态
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled { // 数据竞争检测
			race.Acquire(unsafe.Pointer(m))
		}
		return
}

var waitStartTime int64  // 当前goroutine开始等待时间
starving := false        // goroutine当前所处的模式
awoke := false           // 当前goroutine是否被唤醒 
iter := 0                // 自旋迭代的次数
old := m.state           // old 保存当前 mutex 的状态


for {
	// 有冲突,开始自旋状态
	// 当mutex处于锁定非饥饿工作模式且支持自旋操作的时候。其实就是在自旋然后等待别人释放锁,如果有人释放锁,则会立刻进行下面的尝试获取锁的逻辑
	if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
		// 将 mutex.state 的倒数第二位设置为1,用来告知 Unlock 操作,存在 goroutine 即将得到锁,不需要唤醒其他goroutine
		// 当前线程没有处于唤醒状态,当前锁处于唤醒状态,当前有正在等待的goroutine
		if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
			atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
			awoke = true
		}
		// 进入自旋
		runtime_doSpin()
		iter++
		old = m.state
		continue
	}
	
	// 有冲突,且已经过了自旋阶段。走到这里出现三种情况:1. 当前锁是未锁定状态;2. 锁是饥饿模式;3. 自旋超过指定的次数,不再允许自旋。
	
	new := old
	// 1. 如果当前不是饥饿模式,则这里其实就可以尝试进行锁的获取了|=其实就是将锁的那个bit位设为1表示锁定状态
	if old&mutexStarving == 0 {
		new |= mutexLocked
	}
	// 当mutex处于加锁或饥饿状态的时候,新到来的goroutine进入等待队列
    // 2.此处需要判断是否为加锁状态,因为从1到2的时候可能mutex 重新被其他goroutine加锁了
	if old&(mutexLocked|mutexStarving) != 0 {
		new += 1 << mutexWaiterShift //等待队列加1操作
	}
	
	// 如果当前goroutine已经处于饥饿状态,并且当前锁还是被占用,则锁尝试进行饥饿模式的切换,但如果当前 mutex 未锁定,则不需要切换。Unlock操作希望饥饿模式存在等待者
	// 3.starving条件是为了防止: 如果在2处判断mutex没有处于加锁,而在这里判断mutex却加锁了,这时候加入饥饿模式,可是goroutine没有入列
	if starving && old&mutexLocked != 0 {
		new |= mutexStarving
	}
	// awoke为true则表明当前线程在上面自旋的时候,修改mutexWoken状态成功,清除唤醒标志位
	// 为什么要清除标志位呢?实际上是因为后续流程很有可能当前线程会被挂起,就需要等待其他释放锁的goroutine来唤醒
	// 但如果unlock的时候发现mutexWoken的位置不是0,则就不会去唤醒,则该线程就无法再醒来加锁
	if awoke {
		if new&mutexWoken == 0 {
			throw("sync: inconsistent mutex state")
		}
		// &^操作:后面对应的位为1,则清0前面对应的位,若后面对应的位为0,则前面对应的位保持不变
		new &^= mutexWoken
	}
	// 调用CAS更新state状态
	if atomic.CompareAndSwapInt32(&m.state, old, new) {
		 // 如果原来的状态等于0则表明当前已经释放了锁并且也不处于饥饿模式下,表明可以加锁成功,退出CAS
		if old&(mutexLocked|mutexStarving) == 0 {
			break 
		}
		// 排队逻辑,如果发现waitStatrTime不为0,则表明当前线程之前已经在排队来,后面可能因为unlock被唤醒,但是本次依旧没获取到锁,所以就将它移动到等待队列的头部
		queueLifo := waitStartTime != 0
		if waitStartTime == 0 {
			// 记录开始等待时间
			waitStartTime = runtime_nanotime()
		}
		// 将被唤醒却没得到锁的 goroutine 插入当前等待队列的最前端
		runtime_SemacquireMutex(&m.sema, queueLifo, 1)
		// 如果当前goroutine等待时间超过starvationThresholdNs,mutex 进入饥饿模式
		starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
		// 重新获取状态
		old = m.state
		// 如果发现当前已经是饥饿模式,注意饥饿模式唤醒的是第一个goroutine
		if old&mutexStarving != 0 {
			// 一致性检查
			if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
				throw("sync: inconsistent mutex state")
			}
			delta := int32(mutexLocked - 1<<mutexWaiterShift)
			 // 如果不是饥饿模式了或者当前等待着只剩下一个,退出饥饿模式
			if !starving || old>>mutexWaiterShift == 1 {
				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方法实现

func (m *Mutex) Unlock() {
    if race.Enabled {
        _ = m.state
        race.Release(unsafe.Pointer(m))
    }
    // 直接进行cas操作: mutex 的state减去1, 加锁状态 -> 未加锁
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if new != 0 {
	    if (new+mutexLocked)&mutexLocked == 0 {
	        throw("sync: unlock of unlocked mutex")
	    }
		// 正常模式释放
	    if new&mutexStarving == 0 {
	        old := new
	        for {
				// 如果没有等待者,或者已经存在一个 goroutine 被唤醒或得到锁,或处于饥饿模式,无需唤醒任何处于等待状态的 goroutine
				// 因为lock方法存在自旋一直在获取锁,所以可能解锁后就已经有goroutine获取到锁了
	            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
	                return
	            }
	            // 减去一个等待计数,然后将当前模式切换成mutexWoken
	            new = (old - 1<<mutexWaiterShift) | mutexWoken
	            if atomic.CompareAndSwapInt32(&m.state, old, new) {
	               // 随机唤醒一个阻塞的 goroutine
	                runtime_Semrelease(&m.sema, false)
	                return
	            }
	            old = m.state
	        }
	    // 饥饿模式唤醒
	    } else {
	        // 唤醒第一个等待的线程
	        runtime_Semrelease(&m.sema, true)
	    }
    } 
}

参考文献

golang mutex源码详细解析
golang之sync.Mutex互斥锁源码分析
go sync.Mutex 设计思想与演化过程 (一)
sync包 mutex源码阅读
图解Go里面的互斥锁mutex了解编程语言核心实现源码

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章