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)
}
相关推荐
lhldsg11 分钟前
全民健身解决方案小程序开发:从0到1的技术实战
java·数据库·需求分析
jason成都17 分钟前
Spring WebFlux 适配达梦新方案|dm‑r2dbc:Netty 异步传输的实验性 R2DBC 驱动
java·后端·spring
泡海椒26 分钟前
内置SPI函数库详解:JQuick-Java Builtin工具类实战用法
java·开发语言·python
辰烨chenye35 分钟前
LeetCode Hot 100 题解 · 二分篇
java·算法·leetcode
微功夫信息技术43 分钟前
分层多智能体强化学习驱动的非急救转运公平 - 效率统一调度系统研究与实践
人工智能·学习·算法·动态规划
小羊没烦恼!1 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
dadaobusi2 小时前
学习:开源项目Cheshire
学习
撩得Android一次心动2 小时前
Jetpack Compose 知识点整理1【个人用】
android·学习·kotlin·android jetpack·compose
小荷才露尖尖角,早有蜻蜓立上头2 小时前
过滤器的4种创建方式
java
大圣编蚕2 小时前
Java ByteArrayInputStream 详解:从入门到实战
java·开发语言·算法