Mutex从自旋到饥饿模式
🔍 引言
Go语言的sync.Mutex是标准库中最核心的同步原语之一,其设计经历了多个版本的迭代优化。从最初的朴素互斥锁,到引入自旋优化,再到加入饥饿模式,每一次演进都体现了Go团队对性能和公平性的深度思考。本文将深入剖析Mutex的进化历程,揭示其背后的优化哲学。
1. Mutex锁演进史
1.1 初版实现:朴素互斥锁
最早期的Mutex实现非常简单,基本上就是对系统信号量的直接封装:
// 早期版本的简化实现
type Mutex struct {
sema uint32
}
func (m *Mutex) Lock() {
if !cas(&m.sema, 0, 1) {
semacquire(&m.sema)
}
}
func (m *Mutex) Unlock() {
if !cas(&m.sema, 1, 0) {
semrelease(&m.sema)
}
}
这种实现存在明显的性能问题:
- 无自旋优化:每次锁竞争都直接进入系统调用
- 无公平性保证:新来的goroutine可能抢在等待者前面获得锁
- 调度开销大:频繁的goroutine阻塞和唤醒
1.2 自旋优化:性能的第一次飞跃
为了减少不必要的调度开销,Go引入了自旋优化:
graph TD
A[尝试获取锁] --> B{锁是否可用?}
B -->|是| C[快速获取成功]
B -->|否| D{是否满足自旋条件?}
D -->|是| E[自旋等待]
D -->|否| F[进入等待队列]
E --> G{自旋次数达到限制?}
G -->|否| H[继续自旋]
G -->|是| F
H --> B
F --> I[阻塞等待]
I --> J[被唤醒]
J --> A
自旋优化的核心思想是:对于持有时间很短的锁,与其让goroutine睡眠,不如让它忙等一小段时间。
1.3 饥饿模式:公平性的终极解决方案
现在Mutex的最大创新是引入了饥饿模式,解决了长期存在的公平性问题。让我们详细分析当前的实现:
// go/src/sync/mutex.go
// Mutex 是一个互斥锁
// Mutex的零值是未锁定的mutex
// Mutex在首次使用后不能被复制
type Mutex struct {
state int32 // 状态字段,包含锁定状态、唤醒标志、饥饿模式标志和等待者计数
sema uint32 // 信号量,用于阻塞和唤醒goroutine
}
// 状态位的定义及二进制表示
const (
mutexLocked = 1 << iota // mutex被锁定 = 0001 (第0位)
mutexWoken // 有goroutine被唤醒 = 0010 (第1位)
mutexStarving // mutex处于饥饿模式 = 0100 (第2位)
mutexWaiterShift = iota // 等待者计数的位移量 = 3 (从第3位开始计数等待者)
// 状态组合示例 (32位int32):
// 31-3位: 等待者计数 | 2位:饥饿 | 1位:唤醒 | 0位:锁定
// 00000000_00000000_00000000_00000000 = 0 (初始状态:未锁定,无等待者)
// 00000000_00000000_00000000_00000001 = 1 (已锁定,无等待者)
// 00000000_00000000_00000000_00000011 = 3 (已锁定+被唤醒)
// 00000000_00000000_00000000_00000101 = 5 (已锁定+饥饿模式)
// 00000000_00000000_00000000_00001001 = 9 (已锁定,1个等待者)
// 00000000_00000000_00000000_00001101 = 13 (已锁定+饥饿模式+1个等待者)
// Mutex公平性机制
//
// Mutex有两种操作模式:正常模式和饥饿模式
// 在正常模式下,等待者按FIFO顺序排队,但被唤醒的等待者不会立即拥有mutex,
// 而是与新到达的goroutine竞争锁的所有权。新到达的goroutine有优势——
// 它们已经在CPU上运行,而且可能有很多个,所以被唤醒的等待者很有可能失败。
// 在这种情况下,它会被排在等待队列的前面。如果等待者获取mutex超过1ms失败,
// 它会将mutex切换到饥饿模式。
//
// 在饥饿模式下,mutex的所有权直接从解锁的goroutine传递给队列前面的等待者。
// 新到达的goroutine不会尝试获取mutex,即使它看起来是解锁的,
// 也不会尝试自旋。相反,它们会将自己排在等待队列的尾部。
//
// 如果等待者获得了mutex的所有权并看到以下情况之一:
// (1) 它是队列中的最后一个等待者,或者 (2) 它等待的时间少于1ms,
// 它会将mutex切换回正常操作模式。
//
// 正常模式具有相当好的性能,因为即使有阻塞的等待者,
// goroutine也可以连续多次获取mutex。
// 饥饿模式对于防止尾部延迟的病态情况很重要。
starvationThresholdNs = 1e6 // 1毫秒
)
2. 源码分析
2.1 状态字段设计
现在Mutex的核心是巧妙的状态字段设计,用一个int32包含了所有必要信息:
graph TB
subgraph "State字段(32位二进制布局)"
A["位31-3: 等待者计数<br/>waiter count"]
B["位2: mutexStarving<br/>饥饿模式标志<br/>0100"]
C["位1: mutexWoken<br/>唤醒标志<br/>0010"]
D["位0: mutexLocked<br/>锁定状态<br/>0001"]
end
subgraph "状态组合示例与二进制表示"
E["state = 0<br/>00000000...00000000<br/>未锁定,无等待者"]
F["state = 1<br/>00000000...00000001<br/>已锁定,无等待者"]
G["state = 3<br/>00000000...00000011<br/>已锁定+被唤醒"]
H["state = 5<br/>00000000...00000101<br/>已锁定+饥饿模式"]
I["state = 9<br/>00000000...00001001<br/>已锁定,1个等待者<br/>(1 + 1<<3)"]
J["state = 13<br/>00000000...00001101<br/>已锁定+饥饿模式+1个等待者<br/>(1 + 4 + 1<<3)"]
end
subgraph "位运算说明"
K["获取等待者数量:<br/>old >> mutexWaiterShift"]
L["增加等待者:<br/>new += 1 << mutexWaiterShift"]
M["检查锁定状态:<br/>old & mutexLocked"]
N["检查饥饿模式:<br/>old & mutexStarving"]
end
2.2 Lock快速路径与慢速路径
2.2.1 快速路径
// Lock 锁定m
// 如果锁已被使用,调用的goroutine会阻塞直到mutex可用
func (m *Mutex) Lock() {
// 快速路径:抢占未锁定的mutex
// 期望值: 0 (00000000...00000000) - 完全未锁定状态
// 新值: mutexLocked = 1 (00000000...00000001) - 设置锁定位
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 慢速路径(轮廓化,以便快速路径可以内联)
m.lockSlow()
}
2.2.2 慢速路径详细分析
Mutex的Lock方法采用了经典的快速路径和慢速路径设计。当快速路径失败时,就会进入复杂的慢速路径:
func (m *Mutex) lockSlow() {
var waitStartTime int64 // 等待开始时间
starving := false // 是否处于饥饿状态
awoke := false // 是否被唤醒
iter := 0 // 自旋迭代次数
old := m.state // 当前状态
for {
// 自旋条件检查:
// old&(mutexLocked|mutexStarving) == mutexLocked
// 即:(old & 0101) == 0001,表示已锁定但非饥饿模式
// 二进制分析:
// - 已锁定但非饥饿:xxxx xxx1 & 0101 = 0001 ✓
// - 未锁定: xxxx xxx0 & 0101 = 0000 ✗
// - 饥饿模式: xxxx x1x1 & 0101 = 0101 ✗
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 自旋有意义
// 尝试设置mutexWoken标志来通知Unlock不要唤醒其他阻塞的goroutine
// 条件检查:
// !awoke: 本goroutine还未设置过唤醒标志
// old&mutexWoken == 0: 当前无唤醒标志 (xxxx xx0x & 0010 = 0000)
// old>>mutexWaiterShift != 0: 有等待者 (等待者计数 > 0)
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
// 设置唤醒标志:old | mutexWoken
// 例:00001001 | 0010 = 00001011 (设置第1位)
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// 饥饿模式检查:old&mutexStarving == 0
// 二进制:xxxx x0xx & 0100 = 0000,表示非饥饿模式
if old&mutexStarving == 0 {
// 非饥饿模式下,新来的goroutine可以尝试获取锁
// new |= mutexLocked: 设置锁定位
// 例:00001000 | 0001 = 00001001
new |= mutexLocked
}
// 检查是否需要排队:old&(mutexLocked|mutexStarving) != 0
// 二进制:xxxx xxx & 0101 != 0000,表示已锁定或饥饿模式
if old&(mutexLocked|mutexStarving) != 0 {
// 增加等待者计数:new += 1 << mutexWaiterShift
// 即:new += 1 << 3 = new += 8 (1000)
// 例:00001001 + 00001000 = 00010001 (等待者从1变为2)
new += 1 << mutexWaiterShift
}
// 饥饿模式切换:当前goroutine等待时间过长且mutex仍被锁定
// starving: 本地饥饿状态标志
// old&mutexLocked != 0: mutex当前被锁定
if starving && old&mutexLocked != 0 {
// 设置饥饿模式:new |= mutexStarving
// 例:00010001 | 0100 = 00010101
new |= mutexStarving
}
// 清除唤醒标志:如果本goroutine被唤醒,需要清除标志
if awoke {
// 一致性检查:new应该有唤醒标志
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 清除唤醒标志:new &^= mutexWoken
// &^是位清除运算符:new & (^mutexWoken)
// 例:00010111 &^ 0010 = 00010111 & 1101 = 00010101
new &^= mutexWoken
}
// 原子更新状态
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 检查是否直接获取到锁:old&(mutexLocked|mutexStarving) == 0
// 即之前既未锁定也非饥饿模式:xxxx xxx0 & 0101 = 0000
if old&(mutexLocked|mutexStarving) == 0 {
break // 通过CAS锁定了mutex,成功退出
}
// 需要进入等待队列
// queueLifo:如果之前已经等待过,插入队列头部(LIFO)
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 阻塞等待信号量
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 被唤醒后,检查是否应该进入饥饿模式
// 条件:当前已饥饿 OR 等待时间超过1ms
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 检查是否在饥饿模式下被唤醒:old&mutexStarving != 0
if old&mutexStarving != 0 {
// 饥饿模式下,锁的所有权直接移交给等待者
// 但状态可能不一致,需要修复
// 一致性检查:
// old&(mutexLocked|mutexWoken) != 0: 不应该有锁定或唤醒标志
// old>>mutexWaiterShift == 0: 等待者计数不应该为0
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 计算状态变化:
// delta = mutexLocked - 1<<mutexWaiterShift
// 即:设置锁定位,减少等待者计数
// 例:delta = 0001 - 1000 = -7 (补码:11111001)
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 退出饥饿模式的条件:
// !starving: 本goroutine等待时间不长,OR
// old>>mutexWaiterShift == 1: 这是最后一个等待者
if !starving || old>>mutexWaiterShift == 1 {
// 退出饥饿模式:delta -= mutexStarving
// 即:清除饥饿标志位
// 例:delta = 11111001 - 0100 = 11110101
delta -= mutexStarving
}
// 原子更新状态
// 例:如果old=00001100 (饥饿+1个等待者)
// delta=11110101 (-7-4=-11)
// new=00001100+11110101=00000001 (仅锁定位)
atomic.AddInt32(&m.state, delta)
break
}
// 普通模式下被唤醒,重置状态继续竞争
awoke = true
iter = 0
} else {
// CAS失败,重新读取状态
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
2.2.3 自旋源码分析
自旋是Mutex优化的重要组成部分,它通过主动等待来避免goroutine的阻塞和唤醒开销:
runtime_canSpin 自旋条件检查:
// runtime_canSpin 检查是否可以进行自旋等待
// 在 runtime/proc.go 中实现
func runtime_canSpin(iter int) bool {
// 自旋条件严格限制,避免浪费CPU:
// 1. 迭代次数少于4次 - 避免过度自旋
// 2. 运行在多核机器上 - 单核自旋无意义
// 3. GOMAXPROCS > 1 - 确保有其他P可以运行
// 4. 本地运行队列为空 - 避免阻塞其他goroutine
// 5. 全局运行队列为空 - 系统整体负载不高
return iter < 4 && runtime.NumCPU() > 1 && runtime.GOMAXPROCS(0) > 1 &&
runtime_localRunqEmpty() && runtime_globalRunqEmpty()
}
runtime_doSpin 执行自旋:
// runtime_doSpin 执行处理器级别的自旋
// 在 runtime/asm_amd64.s 中实现
func runtime_doSpin() {
// 执行 PAUSE 指令,优化自旋循环:
// 1. 降低功耗
// 2. 减少对其他超线程的影响
// 3. 给流水线一个提示,这是自旋等待
// 实际执行约 30 个 PAUSE 指令
procyield(30)
}
自旋逻辑在lockSlow中的应用:
for {
// 自旋条件:已锁定但非饥饿模式,且运行时允许自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 尝试设置mutexWoken标志,通知Unlock跳过唤醒
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin() // 执行自旋等待
iter++ // 增加自旋计数
old = m.state // 重新读取状态
continue // 继续自旋循环
}
// 自旋结束,进入等待队列逻辑...
}
2.2.4 信号量基本介绍(后续章节详细讲解)
Go的Mutex底层依赖信号量机制来实现goroutine的阻塞和唤醒:
runtime_SemacquireMutex - 信号量获取:
// runtime_SemacquireMutex 在信号量上等待
// 参数说明:
// - sema: 信号量地址
// - lifo: 是否后进先出(LIFO)排队
// * false: FIFO,新等待者排在队尾
// * true: LIFO,重新等待者排在队头,避免饥饿
// - skipframes: 跳过的栈帧数(用于调试)
func runtime_SemacquireMutex(sema *uint32, lifo bool, skipframes int) {
// 1. 尝试快速获取信号量(原子减一)
// 2. 如果失败,将当前goroutine加入等待队列
// 3. 调用gopark()阻塞当前goroutine
// 4. 等待被runtime_Semrelease唤醒
}
runtime_Semrelease - 信号量释放:
// runtime_Semrelease 释放信号量并唤醒等待者
// 参数说明:
// - sema: 信号量地址
// - handoff: 是否直接移交CPU控制权
// * false: 正常模式,被唤醒者加入运行队列等待调度
// * true: 饥饿模式,当前goroutine立即让出CPU给被唤醒者
// - skipframes: 跳过的栈帧数
func runtime_Semrelease(sema *uint32, handoff bool, skipframes int) {
// 1. 原子增加信号量计数
// 2. 从等待队列中取出一个等待者
// 3. 根据handoff参数决定调度策略:
// - handoff=false: 将被唤醒者加入运行队列
// - handoff=true: 立即切换到被唤醒者执行
}
handoff=true的关键作用:
handoff=true是饥饿模式的核心机制:
- 立即移交:当前解锁的goroutine(G1)调用
runtime_Semrelease后不会立即返回 - CPU让出:G1在
runtime_Semrelease内部调用goyield()主动让出CPU - 直接执行:被唤醒的goroutine(G2)立即获得CPU执行权
- 暂停等待:G1暂停在
runtime_Semrelease调用处,等待下次调度 - 公平保证:确保等待最久的goroutine优先执行,彻底解决饥饿问题
// handoff=true时的执行流程
func unlockSlow(new int32) {
if new&mutexStarving != 0 {
// 饥饿模式:直接移交所有权
runtime_Semrelease(&m.sema, true, 1)
// ↑ 注意:此调用不会立即返回!
// ↓ G1暂停,G2立即执行
// 只有当G2完成后,G1才从这里继续执行
return
}
}
2.2.5 mutexWoken字段深度解析
mutexWoken是Mutex中的关键优化标志,用于避免不必要的goroutine唤醒:
核心问题:
- 如果每次Unlock都唤醒等待者,即使有goroutine正在自旋,会造成资源浪费
- 被唤醒的goroutine可能立即再次阻塞,增加调度开销
解决方案:
// 在lockSlow自旋时设置mutexWoken
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true // 标记已设置唤醒标志
}
工作机制:
- 自旋时设置:正在自旋的goroutine检测到有等待者时,设置
mutexWoken标志 - Unlock检查:Unlock操作检查到此标志,跳过唤醒操作
- 获取后清除:自旋goroutine获得锁后清除此标志
效果对比:
| 场景 | 无mutexWoken | 有mutexWoken |
|---|---|---|
| 系统调用 | 每次可能唤醒 | 智能跳过唤醒 |
| 调度开销 | 频繁切换 | 减少无效切换 |
| 整体性能 | 较低 | 显著提升 |
2.2.6 Lock操作完整流程图
graph TD
A[开始Lock] --> B{快速路径: state==0?}
B -->|是| C[CAS成功获取锁]
B -->|否| D[进入lockSlow慢速路径]
D --> E[初始化变量]
E --> F{自旋条件检查}
F -->|满足| G[设置mutexWoken标志]
G --> H[执行自旋等待]
H --> I{锁是否可用?}
I -->|否| F
I -->|是| J[CAS获取锁]
F -->|不满足| K[构造新状态]
K --> L{非饥饿模式?}
L -->|是| M[尝试设置锁定位]
L -->|否| N[直接排队]
M --> O{需要排队?}
N --> O
O -->|是| P[增加等待者计数]
O -->|否| Q[直接CAS]
P --> R{设置饥饿模式?}
R -->|是| S[设置饥饿标志]
R -->|否| T[CAS更新状态]
S --> T
Q --> T
T --> U{CAS成功?}
U -->|否| V[重新读取状态]
V --> F
U -->|是| W{直接获取成功?}
W -->|是| X[Lock成功]
W -->|否| Y[进入等待队列]
Y --> Z[SemacquireMutex阻塞]
Z --> AA[被唤醒]
AA --> BB{饥饿模式唤醒?}
BB -->|是| CC[直接获得所有权]
CC --> DD{退出饥饿条件?}
DD -->|是| EE[退出饥饿模式]
DD -->|否| FF[保持饥饿模式]
EE --> X
FF --> X
BB -->|否| GG[重新竞争]
GG --> F
C --> X
J --> X
2.2.7 Lock场景详细分析
场景1:自旋获取锁
模拟数据:
- G1持有锁,执行短暂操作(1纳秒)
- G2到达时开始自旋
- G1快速释放,G2自旋成功获取
graph TD
subgraph "步骤1: G1获取锁"
A1["状态: 00000001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B1["G1执行临界区"]
end
subgraph "步骤2: G2开始自旋"
A2["状态: 00000001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B2["G2检查自旋条件<br/>✓ 已锁定非饥饿<br/>✓ runtime_canSpin<br/>开始自旋"]
end
subgraph "步骤3: G1解锁(1ns后)"
A3["状态: 00000000<br/>锁定:✗ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B3["G1快速路径解锁<br/>new=0,直接返回"]
end
subgraph "步骤4: G2自旋成功"
A4["状态: 00000001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B4["G2检测到状态变化<br/>CAS成功获取锁"]
end
A1 --> A2
A2 --> A3
A3 --> A4
场景2:正常模式获取
模拟数据:
- 已有1个等待者G2在队列中
- G3到达,自旋失败,进入队列
- G1解锁,按FIFO顺序唤醒G2
graph TD
subgraph "步骤1: 初始状态"
A1["状态: 00001001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:1<br/>队列:[G2]"]
B1["G1持有锁<br/>G2在队列等待"]
end
subgraph "步骤2: G3尝试获取"
A2["状态: 00001001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:1<br/>队列:[G2]"]
B2["G3自旋条件检查<br/>✗ 不满足条件<br/>准备进入队列"]
end
subgraph "步骤3: G3进入队列"
A3["状态: 00010001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:2<br/>队列:[G2,G3]"]
B3["new += 1<<mutexWaiterShift<br/>CAS更新成功<br/>调用SemacquireMutex"]
end
subgraph "步骤4: G1解锁唤醒G2"
A4["状态: 00001010<br/>锁定:✗ 唤醒:✓ 饥饿:✗<br/>等待者:1<br/>队列:[G3]"]
B4["G1调用unlockSlow<br/>设置mutexWoken<br/>Semrelease(handoff=false)<br/>G2被唤醒竞争"]
end
A1 --> A2
A2 --> A3
A3 --> A4
场景3:进入饥饿状态
模拟数据:
- G2已等待超过1ms
- G4到达检测到饥饿情况
- 系统切换到饥饿模式
graph TD
subgraph "步骤1: 长时间等待"
A1["状态: 00010001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:2<br/>队列:[G2🕐>1ms,G3]"]
B1["G2等待时间>1ms<br/>G1仍持有锁"]
end
subgraph "步骤2: G4检测饥饿"
A2["状态: 00010001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:2<br/>队列:[G2🕐>1ms,G3]"]
B2["G4到达lockSlow<br/>发现等待时间>1ms<br/>设置starving=true"]
end
subgraph "步骤3: 切换饥饿模式"
A3["状态: 00011101<br/>锁定:✓ 唤醒:✗ 饥饿:✓<br/>等待者:3<br/>队列:[G2🕐>1ms,G3,G4]"]
B3["new |= mutexStarving<br/>new += 1<<mutexWaiterShift<br/>CAS成功更新"]
end
subgraph "步骤4: 饥饿模式生效"
A4["状态: 00011101<br/>锁定:✓ 唤醒:✗ 饥饿:✓<br/>等待者:3<br/>队列:[G2🕐>1ms,G3,G4]"]
B4["新来者直接排队<br/>不再尝试自旋或竞争<br/>严格FIFO顺序"]
end
A1 --> A2
A2 --> A3
A3 --> A4
场景4:退出饥饿状态
模拟数据:
- 饥饿模式下最后一个等待者G2被唤醒
- G2获得锁后检查退出条件
- 切换回正常模式
graph TD
subgraph "步骤1: 饥饿模式解锁"
A1["状态: 00001100<br/>锁定:✗ 唤醒:✗ 饥饿:✓<br/>等待者:1<br/>队列:[G2]"]
B1["G1解锁调用<br/>unlockSlow<br/>handoff=true"]
end
subgraph "步骤2: G2被直接唤醒"
A2["状态: 00001100<br/>锁定:✗ 唤醒:✗ 饥饿:✓<br/>等待者:1<br/>队列:[G2🔄]"]
B2["Semrelease(handoff=true)<br/>G1让出CPU<br/>G2立即执行"]
end
subgraph "步骤3: G2检查退出条件"
A3["状态: 00001100<br/>锁定:✗ 唤醒:✗ 饥饿:✓<br/>等待者:1<br/>队列:[G2🔄]"]
B3["G2在lockSlow中<br/>检测饥饿模式唤醒<br/>old&mutexStarving != 0"]
end
subgraph "步骤4: 退出饥饿模式"
A4["状态: 00000001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B4["最后一个等待者<br/>delta = mutexLocked-1<<shift-mutexStarving<br/>atomic.AddInt32更新<br/>恢复正常模式"]
end
A1 --> A2
A2 --> A3
A3 --> A4
2.3 Unlock快速路径与慢速路径
2.3.1 快速路径
// Unlock 解锁m
// 如果m在进入Unlock时没有被锁定,这是一个运行时错误
// 被锁定的Mutex不与特定的goroutine关联
// 允许一个goroutine锁定Mutex,然后安排另一个goroutine解锁它
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 快速路径:清除锁定位
// atomic.AddInt32(&m.state, -mutexLocked) 等价于 &m.state -= 1
// 例:00010001 + 11111111 = 00010000 (去除锁定位)
new := atomic.AddInt32(&m.state, -mutexLocked)
// 检查是否需要慢速路径:new != 0
// 如果new == 0,说明解锁前状态只有锁定位,无等待者和其他标志
// 如果new != 0,说明还有等待者或其他标志需要处理
if new != 0 {
// 轮廓化慢速路径以允许内联快速路径
// 为了在跟踪期间隐藏unlockSlow,当跟踪GoUnblock时我们跳过一个额外的帧
m.unlockSlow(new)
}
}
2.3.2 慢速路径详细分析
func (m *Mutex) unlockSlow(new int32) {
// 验证mutex之前确实被锁定:
// (new + mutexLocked) & mutexLocked == 0
// 即:(new + 1) & 1 == 0,说明new的最低位为1,即之前未锁定
// 例:如果new=00010000,new+1=00010001,&1=1,说明之前已锁定 ✓
// 如果new=00010001,new+1=00010010,&1=0,说明之前未锁定 ✗
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
// 检查是否处于饥饿模式:new&mutexStarving == 0
// 二进制:xxxx x0xx & 0100 = 0000,表示非饥饿模式
if new&mutexStarving == 0 {
// 正常模式:需要竞争唤醒等待者
old := new
for {
// 检查是否需要唤醒等待者:
// 1. old>>mutexWaiterShift == 0:无等待者
// 2. old&(mutexLocked|mutexWoken|mutexStarving) != 0:
// 已被锁定 OR 已有goroutine被唤醒 OR 处于饥饿模式
//
// 二进制分析:
// - 无等待者: xxxx 0000 >> 3 = 0000
// - 已锁定: xxxx xxx1 & 0111 = xxx1 != 0
// - 已唤醒: xxxx xx1x & 0111 = xx1x != 0
// - 饥饿模式: xxxx x1xx & 0111 = x1xx != 0
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return // 无需唤醒,直接返回
}
// 获得唤醒权利,构造新状态:
// new = (old - 1<<mutexWaiterShift) | mutexWoken
// 即:等待者计数-1,设置唤醒标志
// 例:old=00010000 (1个等待者)
// old - 1<<3 = 00010000 - 00001000 = 00001000 (0个等待者)
// new = 00001000 | 0010 = 00001010 (设置唤醒标志)
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 成功设置唤醒标志,释放一个等待的goroutine
// handoff=false: 不直接移交所有权,让被唤醒者竞争
runtime_Semrelease(&m.sema, false, 1)
return
}
// CAS失败,重新读取状态
old = m.state
}
} else {
// 饥饿模式:直接移交所有权给下一个等待者
//
// 饥饿模式特点:
// 1. mutex所有权直接从解锁goroutine移交给队列头部等待者
// 2. 不设置mutexLocked位,由被唤醒者设置
// 3. mutexStarving位保持设置,被唤醒者决定是否退出饥饿模式
// 4. 新来的goroutine不会尝试获取锁,直接排队
//
// 状态变化:
// 解锁前:xxxx x1x1 (饥饿+锁定)
// 解锁后:xxxx x1x0 (饥饿但未锁定,等待被唤醒者设置锁定位)
// handoff=true: 直接移交所有权,被唤醒者不需要竞争
runtime_Semrelease(&m.sema, true, 1)
}
}
2.3.3 Unlock操作详细步骤分析
第一阶段:快速路径解锁
- 原子清除锁定位:使用
atomic.AddInt32(&m.state, -mutexLocked)清除锁定位 - 零状态检查:检查操作后的新状态是否为零
- 快速完成:如果新状态为零,说明没有等待者和其他标志,直接完成解锁并返回
第二阶段:慢速路径初始化 4. 进入慢速路径:新状态非零,说明存在等待者或其他标志需要处理 5. 状态合法性验证:验证mutex之前确实处于锁定状态
- 通过检查
(新状态+锁定位)&锁定位 == 0来验证 - 如果验证失败,触发致命错误(解锁未锁定的mutex)
第三阶段:模式判断与分发 6. 饥饿模式检查:检查新状态是否包含饥饿模式标志 7. 路径分发:根据模式选择不同的处理路径
- 非饥饿模式:需要竞争性地唤醒等待者
- 饥饿模式:直接移交所有权给下一个等待者
第四阶段:正常模式处理循环 8. 状态基准设定:以当前状态作为处理基准 9. 唤醒条件检查:检查是否需要唤醒等待者
- 无等待者:等待者计数为零
- 已有处理:已锁定、已唤醒或已进入饥饿模式
- 直接返回判断:如果满足任一条件,无需唤醒,直接返回
第五阶段:正常模式唤醒执行
11. 新状态构造:构造唤醒操作的新状态
- 减少等待者计数:old - 1<<mutexWaiterShift
- 设置唤醒标志:| mutexWoken
12. 原子状态更新:尝试原子性地更新状态
13. 更新成功处理:如果原子操作成功,释放信号量唤醒一个等待者
- 使用非移交模式(handoff=false)
- 被唤醒的goroutine需要与其他goroutine竞争
14. 更新失败处理:如果原子操作失败,重新读取状态并重试
第六阶段:饥饿模式直接移交
15. 直接所有权移交:在饥饿模式下,直接将mutex所有权移交给队列头部等待者
16. 移交模式释放:使用移交模式释放信号量(handoff=true)
- 被唤醒的goroutine直接获得所有权,无需竞争
- 不设置锁定位,由被唤醒者负责设置
- 保持饥饿模式标志,由被唤醒者决定是否退出饥饿模式
2.3.4 Unlock操作完整流程图
graph TD
A[开始Unlock] --> B[原子操作AddInt32减1]
B --> C{new == 0?}
C -->|是| D[快速路径完成]
C -->|否| E[进入unlockSlow]
E --> F[验证锁定状态]
F --> G{检查饥饿模式}
G -->|否| H[正常模式处理]
G -->|是| I[饥饿模式处理]
H --> J{需要唤醒等待者?}
J -->|否| K[直接返回]
J -->|是| L[构造新状态]
L --> M[CAS更新状态]
M --> N{CAS成功?}
N -->|否| O[重新读取状态]
O --> J
N -->|是| P[Semrelease handoff=false]
P --> Q[唤醒等待者竞争]
Q --> K
I --> R[Semrelease handoff=true]
R --> S[直接移交所有权]
S --> T[当前goroutine让出CPU]
T --> K
D --> U[Unlock完成]
K --> U
2.3.5 Unlock场景详细分析
场景1:快速路径解锁
模拟数据:
- 仅有锁定位,无等待者和其他标志
- 解锁后状态直接变为0
graph TD
subgraph "解锁前状态"
A1["状态: 00000001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B1["G1持有锁<br/>准备解锁"]
end
subgraph "快速路径执行"
A2["原子操作: AddInt32(-1)<br/>00000001 + 11111111<br/>= 00000000"]
B2["new == 0<br/>无需慢速路径"]
end
subgraph "解锁后状态"
A3["状态: 00000000<br/>锁定:✗ 唤醒:✗ 饥饿:✗<br/>等待者:0<br/>队列:[]"]
B3["解锁完成<br/>mutex完全空闲"]
end
A1 --> A2
A2 --> A3
场景2:正常模式唤醒等待者
模拟数据:
- 有2个等待者在队列中
- 正常模式下竞争性唤醒
graph TD
subgraph "解锁前状态"
A1["状态: 00010001<br/>锁定:✓ 唤醒:✗ 饥饿:✗<br/>等待者:2<br/>队列:[G2,G3]"]
B1["G1持有锁<br/>准备解锁"]
end
subgraph "进入慢速路径"
A2["new = 00010000<br/>锁定:✗ 唤醒:✗ 饥饿:✗<br/>等待者:2"]
B2["new != 0<br/>进入unlockSlow"]
end
subgraph "正常模式处理"
A3["检查唤醒条件:<br/>等待者>0 且无其他标志<br/>构造新状态"]
B3["new = (old-8)|2<br/>= 00001010<br/>等待者-1+设置woken"]
end
subgraph "解锁后状态"
A4["状态: 00001010<br/>锁定:✗ 唤醒:✓ 饥饿:✗<br/>等待者:1<br/>队列:[G3]"]
B4["Semrelease(handoff=false)<br/>G2被唤醒参与竞争"]
end
A1 --> A2
A2 --> A3
A3 --> A4
场景3:饥饿模式直接移交
模拟数据:
- 饥饿模式下有1个等待者
- 直接移交所有权给等待者
graph TD
subgraph "解锁前状态"
A1["状态: 00001101<br/>锁定:✓ 唤醒:✗ 饥饿:✓<br/>等待者:1<br/>队列:[G2]"]
B1["G1持有锁<br/>饥饿模式"]
end
subgraph "进入慢速路径"
A2["new = 00001100<br/>锁定:✗ 唤醒:✗ 饥饿:✓<br/>等待者:1"]
B2["检测到饥饿模式<br/>new&mutexStarving != 0"]
end
subgraph "饥饿模式处理"
A3["直接调用:<br/>Semrelease(handoff=true)"]
B3["G1让出CPU<br/>G2立即获得执行权"]
end
subgraph "移交后状态"
A4["状态: 00001100<br/>锁定:✗ 唤醒:✗ 饥饿:✓<br/>等待者:1<br/>队列:[G2🔄执行中]"]
B4["G2将在lockSlow中<br/>设置锁定位并决定<br/>是否退出饥饿模式"]
end
A1 --> A2
A2 --> A3
A3 --> A4
场景4:无需唤醒的情况
模拟数据:
- 已有woken标志或已被其他goroutine锁定
- 跳过唤醒操作
graph TD
subgraph "解锁前状态"
A1["状态: 00001011<br/>锁定:✓ 唤醒:✓ 饥饿:✗<br/>等待者:1<br/>队列:[G2]"]
B1["G1持有锁<br/>已有woken标志<br/>(可能G3在自旋)"]
end
subgraph "进入慢速路径"
A2["new = 00001010<br/>锁定:✗ 唤醒:✓ 饥饿:✗<br/>等待者:1"]
B2["检测到woken标志<br/>old&0111 != 0"]
end
subgraph "跳过唤醒"
A3["条件不满足:<br/>已有woken标志"]
B3["直接返回<br/>无需重复唤醒"]
end
subgraph "解锁后状态"
A4["状态: 00001010<br/>锁定:✗ 唤醒:✓ 饥饿:✗<br/>等待者:1<br/>队列:[G2]"]
B4["G3自旋中<br/>将获取锁并清除woken"]
end
A1 --> A2
A2 --> A3
A3 --> A4
7. 总结
通过深入剖析Mutex的进化历程,我们可以看到Go团队在设计同步原语时,如何权衡公平性和性能,以及如何通过自旋优化和饥饿模式来提升系统的整体性能。希望本文能够帮助读者更好地理解Mutex的设计哲学和实现细节。