sync.mutex 锁详解
基于 Go 语言 1.15 版本
一、互斥锁的性质
互斥锁有两种状态:正常状态和饥饿状态
在正常状态下,所有等待锁的 Goroutine 按照 FIFO(先进先出)的顺序等待。唤醒的 Goroutine 不会直接拥有锁,而是会和新请求锁的 Goroutine 竞争锁的拥有。新请求锁的 Goroutine 具有优势,他正在 CPU 上执行,而且可能有好几个,所以刚刚唤醒的 Goroutine 有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 Goroutine 会加入到等待队列的前面。如果一个等待的 Goroutine 超过 1ms 没有获取到锁,那么他将会把锁变为饥饿模式
在饥饿模式下,锁的所有权会直接交给等待队列最前面的 Goroutine。新来的 Goroutine 在该状态下将不会尝试去获取锁,即使锁看起来是 unlock 状态,也不会尝试自旋操作,而是放在等待队列的尾部
如果一个等待的 Goroutine 获取了锁,并且满足以下其中的任何一个条件,他会将锁的状态转换为正常状态
- 这个 Goroutine 是队列中的最后一个
- 这个 Goroutine 等待的时间小于 1ms
正常状态有很好的性能表现,饥饿模式也是非常重要的,因为他能阻止尾部延迟的现象
二、源码解析
sync.mutex 的结构
1 2 3 4
| type Mutex struct { state int32 sema uint32 }
|
- state:一个共用的字段,第 0 个 bit 标记这个 mutex 是否已被某个 Goroutine 所拥有。如果第 0 个 bit 为 0 ,则没有被锁,此 mutex 目前没有被某个 Goroutine 所拥有
- 第 1 个 bit 标记这个 mutex 是否已唤醒,也就是有个唤醒的 Goroutine 要尝试获取锁
- 第 2 个 bit 标记这个 mutex 状态,值为 1 表明此锁已处于饥饿状态
同时,尝试获取锁的 Goroutine 也有状态,有可能它是新来的 Goroutine,也有可能是被唤醒的goroutine, 可能是处于正常状态的goroutine, 也有可能是处于饥饿状态的goroutine。
1. Lock 加锁
- 如果互斥锁处于初始化状态,会通过置位 mutexLocked 加锁
- 如果互斥锁处于
mutexLocked
状态并且在普通模式下工作,会进入自旋,执行 30 次 PAUSE
指令消耗 CPU 时间等待锁的释放
- 如果当前 Goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式
- 互斥锁在正常情况下会将尝试获取锁的 Goroutine 切换至休眠状态,等待锁的持有者唤醒
- 如果当前 Goroutine 是互斥锁上的最后一个等待的协程或者等待的时间小于 1ms,那么它会将互斥锁切换回正常模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
| func (m *Mutex) Lock() { if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { return } var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state for { if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { 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 if old&mutexStarving == 0 { new |= mutexLocked } if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift } if starving && old&mutexLocked != 0 { new |= mutexStarving } if awoke { if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } new &^= mutexWoken } if atomic.CompareAndSwapInt32(&m.state, old, new) { if old&(mutexLocked|mutexStarving) == 0 { break } queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } runtime_SemacquireMutex(&m.sema, queueLifo) starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs old = m.state 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 } } }
|
2. UnLock 解锁
- 当互斥锁已经被解锁时,调用
sync.Mutex.Unlock
会直接抛出异常
- 当互斥锁处于饥饿模式时,将锁的所有权交给队列中的下一个等待者,等待者会负责设置
mutexLocked
标志位
- 当互斥锁处于普通模式时,如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁,会直接返回;在其他情况下会唤醒对应的 Goroutine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| func (m *Mutex) Unlock() { new := atomic.AddInt32(&m.state, -mutexLocked) if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } 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) return } old = m.state } } else { runtime_Semrelease(&m.sema, true) } }
|
三、案例
如果有一个 Goroutine(G1)通过 Lock 获取了锁,在持有锁的期间,另外一个 Goroutine(G2) 调用了 Unlock 释放了这个锁,那么 G2 调用 Unlock 成功,但是如果将来 G1 调用 Unlock 会 panic