Golang sync.Mutex实现原理学习

按 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)
}
相关推荐
Canmag 兴隆磁性5 分钟前
自动装盒设备 AO-WCMF
学习·永磁材料·充磁
imDwAaY5 分钟前
Bean的生命周期
java·笔记·后端·学习·spring·dubbo
不停喝水6 分钟前
【前端转全栈java速通课】 项目实战④-1 Spring Boot 创建项目-定义接口-请求参数处理-分层规范-依赖注入
java·前端·spring boot
坤坤子吖10 分钟前
C++11——Lambda、function、bind 与可调用对象
c++·笔记·学习
上下求索,莫负韶华14 分钟前
Spring全家桶
java·后端·spring
优化Henry16 分钟前
LTE站点以太网光模块告警处理实践
运维·网络·笔记·学习·信息与通信
茉莉玫瑰花茶18 分钟前
GO [ 接口 ]
服务器·数据库·golang
w_zero_one26 分钟前
链表(2)
java·数据结构·链表
白杨尚青33 分钟前
C++入门篇(十一):string(中)——容量与增删改查:一篇吃透所有常用接口(万字详解)
java·开发语言·c++
SimonKing44 分钟前
SSE、WebSocket 连接丢 Redis 里?那可踩大坑了!
java·后端·程序员