结构体定义
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的优势
饥饿模式
- 进入条件:当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上的运行队列是空的
- Mutex状态条件
- 可以自旋的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
- 未处于饥饿模式下会循环直到唤醒一个新的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) // 饥饿模式下,直接唤醒第一个等待者
}
}