按 Go 1.25.10 的 sync.Mutex 实现整理。公开的 sync.Mutex 只是包装,核心代码在
internal/sync/mutex.go
总结
sync.Mutex
┌──────────────────────────────────────────┐
│ state int32 │
│ [ waiter数量 | starving | woken | locked ]
│ │
│ sema uint32 │
│ runtime 挂起/唤醒 goroutine 使用 │
└──────────────────────────────────────────┘
Lock()
│
├─ CAS(0 → locked) 成功 ───────────────> 获得锁
│
└─ CAS失败
│
▼
lockSlow()
│
├─ 允许自旋?
│ ├─ 是:PAUSE,重新读 state,继续尝试
│ └─ 否
│
├─ CAS 更新 state
│ ├─ 锁刚好释放了 ─────────────> 获得锁
│ └─ 锁仍被占用:waiter++
│
▼
runtime_SemacquireMutex
│
▼
gopark 当前 G
│
▼
被唤醒
│
├─ 正常模式:只是获得"重新竞争"的机会
│ 回到循环重新抢锁
│
└─ 饥饿模式:锁直接交给队首 waiter
获得锁
- 无竞争:一次 CAS,用户态完成
- 轻度竞争:有限自旋,等待锁快速释放
- 严重竞争:waiter++,通过 runtime semaphore 挂起 G
- 正常模式:唤醒 waiter 后重新竞争,吞吐量优先
- 等待超过1ms:进入饥饿模式,锁直接交给队首 waiter,公平性优先
- Unlock:
- 无 waiter 时一次原子减法;
- 有 waiter 时唤醒或直接交接
"正常模式 / 饥饿模式"是 Mutex 这把锁的全局工作模式,主要决定等待锁的 goroutine 怎么获得锁。不是单独指"拿锁的"或"等锁的",但模式切换是由等锁太久的 goroutine触发的。
Mutex.state
┌─────────────────────────────────────────┐
│ waiter数量 | starving | woken | locked │
└─────────────────────────────────────────┘
▲
└── starving 是整把锁的状态
对比
正常模式:
Unlock → 唤醒 waiter → waiter 与新来的 G 竞争
↑
吞吐量优先
饥饿模式:
Unlock → 直接把锁交给队首 waiter
↑
公平性优先
等锁的 goroutine 等得太久,会让整把 Mutex 进入饥饿模式;之后持锁者 Unlock 时,直接把锁交给队首等待者。
Mutex 状态位
type Mutex struct {
state int32 // 锁状态、唤醒状态、饥饿状态、等待者数量
sema uint32 // runtime semaphore
}
state:
31 3 2 1 0
┌─────────────────────────┬───────────┬───────┬────────┐
│ waiter 数量 │ starving │ woken │ locked │
└─────────────────────────┴───────────┴───────┴────────┘
定义:
const (
mutexLocked = 1 << iota // bit 0:锁已占用
mutexWoken // bit 1:已经唤醒一个 waiter
mutexStarving // bit 2:饥饿模式
mutexWaiterShift = iota // bit 3以上:waiter 数量
)
Lock 快速路径
func (m *Mutex) Lock() {
// 没有竞争:一次 CAS 把状态从0改成locked
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 有竞争:进入慢速路径
m.lockSlow()
}
lockSlow 自旋阶段
if old&(mutexLocked|mutexStarving) == mutexLocked &&
runtime_canSpin(iter) {
// 如果已经有 waiter,标记"我负责等待/被唤醒"
// 避免 Unlock 再唤醒过多 goroutine
if !awoke &&
old&mutexWoken == 0 &&
old>>mutexWaiterShift != 0 {
CAS(&m.state, old, old|mutexWoken)
}
runtime_doSpin() // amd64 上主要是 PAUSE
iter++
old = m.state
continue
}
允许自旋的主要条件:
- 锁已占用
- 没有进入饥饿模式
- 多核 CPU
- GOMAXPROCS > 1
- 还有其他 P 正在运行
- 当前 P 的本地运行队列为空
- 自旋轮数没有达到上限、
当前实现最多约 4 轮:每轮:PAUSE × 30
自旋主要是 PAUSE + 读取 state。
计算新状态并 CAS
停止自旋后,当前 goroutine 根据旧状态构造新状态:
new := old
// 非饥饿模式:尝试设置 locked
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 锁仍被占用:登记为 waiter
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 当前 G 已经等待超过1ms,请求进入饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 当前 G 之前设置过 woken,现在清掉
if awoke {
new &^= mutexWoken
}
if CAS(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
// CAS 前锁已经释放,这次直接抢到了
break
}
// 锁仍被占用,进入等待队列
runtime_SemacquireMutex(&m.sema, queueLifo, 2)
}
CAS成功
- 旧状态空闲:成功获得锁
- 旧状态繁忙:只是成功登记 waiter,接下来仍要等待
Semaphore 等待
go/src/runtime/sema.go
runtime_SemacquireMutex(&m.sema, queueLifo, 2)
内部
&m.sema
│
▼
runtime semTable
│
▼
加入 sudog 等待队列
│
▼
gopark 当前 goroutine
正常模式与饥饿模式
正常模式
Unlock 唤醒 G2
│
▼
G2 变成 runnable
│
├─ 新来的 G3 已经在 CPU 上,可能先抢到锁
└─ G2 抢锁失败,重新排队
正常模式吞吐量高,但老 waiter 可能连续失败。
饥饿模式
如果某个 waiter 等待超过约 1ms:
正常模式 ──等待超过1ms──> 饥饿模式
饥饿模式下:
新来的 G:
不自旋
不抢锁
直接排到等待队列末尾
Unlock:
直接把锁交给队首 waiter
下面情况会退出饥饿模式:
当前获得锁的是最后一个 waiter
或者
当前 waiter 实际等待时间不足1ms
Unlock 流程
快速路径:
func (m *Mutex) Unlock() {
// 原子清除 locked 位
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
流程
Unlock
│
├─ state 变成 0
│ └─没有 waiter,直接返回
│
└─ state 非0
│
▼
unlockSlow
│
├─正常模式
│ ├─选择一个 waiter
│ ├─设置 woken
│ └─Semrelease(false):唤醒它重新竞争
│
└─饥饿模式
└─Semrelease(true):直接交接锁
正常code
// waiter--,并标记已有一个 waiter 被唤醒
new = (old - 1<<mutexWaiterShift) | mutexWoken
if CAS(&m.state, old, new) {
// 唤醒一个 waiter,让它重新竞争
runtime_Semrelease(&m.sema, false, 2)
return
}
饥饿code
// handoff=true:直接把锁交给队首 waiter
runtime_Semrelease(&m.sema, true, 2)
TryLock
CAS一次 没其他机制
func (m *Mutex) TryLock() bool {
old := m.state
// 已锁定或处于饥饿模式,立即失败
if old&(mutexLocked|mutexStarving) != 0 {
return false
}
// 尝试设置 locked
return CAS(&m.state, old, old|mutexLocked)
}