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)
}
相关推荐
狗凯之家源码网1 小时前
狗凯源码库学习资源真实价值评测
学习
Bernice橘子1 小时前
职场英语学习计划Day024
学习
头发还在的女程序员11 小时前
医院陪诊管理系统怎么选择?——2026 年选型避坑与架构参考
java·开发语言·陪诊系统·陪诊app·医院陪诊陪护
CodeStats11 小时前
【Spring事务】Spring事务注解 @Transactional 完整体系:从 MySQL 隔离级别到 MyBatis 原理详解
java·spring·mybatis·事务·transactional
min(a,b)11 小时前
学习第 4 天:面向对象与异常处理
python·学习·学习方法
一只小菜鸡..11 小时前
南京大学 操作系统 (JYY) 学习笔记:进程地址空间与内存“外挂”魔法
笔记·学习
我命由我1234512 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
tju新生代魔迷12 小时前
Verilog语言学习(一)
学习
一只小菜鸡..12 小时前
南京大学 操作系统 (JYY) 学习笔记:访问操作系统的对象与“一切皆文件”
笔记·学习