undefined

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() {
// 如果mutext的state没有被锁,也没有等待/唤醒的goroutine, 锁处于正常状态,那么获得锁,返回.
// 比如锁第一次被goroutine请求时,就是这种状态。或者锁处于空闲的时候,也是这种状态。
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 标记本goroutine的等待时间
var waitStartTime int64
// 本goroutine是否已经处于饥饿状态
starving := false
// 本goroutine是否已唤醒
awoke := false

// 自旋次数
iter := 0

// 复制锁的当前状态
old := m.state

for {
// 第一个条件是state已被锁,但是不是饥饿状态。如果时饥饿状态,自旋时没有用的,锁的拥有权直接交给了等待队列的第一个。
// 第二个条件是还可以自旋,多核、压力不大并且在一定次数内可以自旋, 具体的条件可以参考`sync_runtime_canSpin`的实现。
// 如果满足这两个条件,不断自旋来等待锁被释放、或者进入饥饿状态、或者不能再自旋。
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 自旋的过程中如果发现state还没有设置woken标识,则设置它的woken标识, 并标记自己为被唤醒。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}

// 到了这一步, state的状态可能是:
// 1. 锁还没有被释放,锁处于正常状态
// 2. 锁还没有被释放, 锁处于饥饿状态
// 3. 锁已经被释放, 锁处于正常状态
// 4. 锁已经被释放, 锁处于饥饿状态
//
// 并且本gorutine的 awoke可能是true, 也可能是false (其它goutine已经设置了state的woken标识)
// new 复制 state的当前状态, 用来设置新的状态
// old 是锁当前的状态
new := old

// 如果old state状态不是饥饿状态, new state 设置锁, 尝试通过CAS获取锁,
// 如果old state状态是饥饿状态, 则不设置new state的锁,因为饥饿状态下锁直接转给等待队列的第一个.
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 将等待队列的等待者的数量加1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}

// 如果当前goroutine已经处于饥饿状态, 并且old state的已被加锁,
// 将new state的状态标记为饥饿状态, 将锁转变为饥饿状态.
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}

// 如果本goroutine已经设置为唤醒状态, 需要清除new state的唤醒标记, 因为本goroutine要么获得了锁,要么进入休眠,
// 总之state的新状态不再是woken状态.
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}

// 通过CAS设置new state值.
// 注意new的锁标记不一定是true, 也可能只是标记一下锁的state是饥饿状态.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果old state的状态是未被锁状态,并且锁不处于饥饿状态,
// 那么当前goroutine已经获取了锁的拥有权,返回
if old&(mutexLocked|mutexStarving) == 0 {
break
}

// 设置/计算本goroutine的等待时间
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}

// 既然未能获取到锁, 那么就使用sleep原语阻塞本goroutine
// 如果是新来的goroutine,queueLifo=false, 加入到等待队列的尾部,耐心等待
// 如果是唤醒的goroutine, queueLifo=true, 加入到等待队列的头部
runtime_SemacquireMutex(&m.sema, queueLifo)

// sleep之后,此goroutine被唤醒
// 计算当前goroutine是否已经处于饥饿状态.
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 得到当前的锁状态
old = m.state

// 如果当前的state已经是饥饿状态
// 那么锁应该处于Unlock状态,那么应该是锁被直接交给了本goroutine
if old&mutexStarving != 0 {

// 如果当前的state已被锁,或者已标记为唤醒, 或者等待的队列中不为空,
// 那么state是一个非法状态
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}

// 当前goroutine用来设置锁,并将等待的goroutine数减1.
delta := int32(mutexLocked - 1<<mutexWaiterShift)

// 如果本goroutine是最后一个等待者,或者它并不处于饥饿状态,
// 那么我们需要把锁的state状态设置为正常模式.
if !starving || old>>mutexWaiterShift == 1 {
// 退出饥饿模式
delta -= mutexStarving
}

// 设置新state, 因为已经获得了锁,退出、返回
atomic.AddInt32(&m.state, delta)
break
}

// 如果当前的锁是正常模式,本goroutine被唤醒,自旋次数清零,从for循环开始处重新开始
awoke = true
iter = 0
} else { // 如果CAS不成功,重新获取锁的state, 从for循环开始处重新开始
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() {
// 如果state不是处于锁的状态, 那么就是Unlock根本没有加锁的mutex, panic
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}

// 释放了锁,还得需要通知其它等待者
// 锁如果处于饥饿状态,直接交给等待队列的第一个, 唤醒它,让它去获取锁
// 锁如果处于正常状态,
// new state如果是正常状态
if new&mutexStarving == 0 {
old := new
for {
// 如果没有等待的goroutine, 或者锁不处于空闲的状态,直接返回.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 将等待的goroutine数减一,并设置woken标识
new = (old - 1<<mutexWaiterShift) | mutexWoken
// 设置新的state, 这里通过信号量会唤醒一个阻塞的goroutine去获取锁.
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// 饥饿模式下, 直接将锁的拥有权传给等待队列中的第一个.
// 注意此时state的mutexLocked还没有加锁,唤醒的goroutine会设置它。
// 在此期间,如果有新的goroutine来请求锁, 因为mutex处于饥饿状态, mutex还是被认为处于锁状态,
// 新来的goroutine不会把锁抢过去.
runtime_Semrelease(&m.sema, true)
}
}

三、案例

如果有一个 Goroutine(G1)通过 Lock 获取了锁,在持有锁的期间,另外一个 Goroutine(G2) 调用了 Unlock 释放了这个锁,那么 G2 调用 Unlock 成功,但是如果将来 G1 调用 Unlock 会 panic