【Go】Mutex
点击阅读更多查看文章内容
基本原语
Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.Once 和 sync.Cond:
这些基本原语提供了较为基础的同步功能,但是它们是一种相对原始的同步机制,在多数情况下,我们都应该使用抽象层级更高的 Channel 实现同步。
Mutex
Go 语言的 sync.Mutex 由两个字段 state 和 sema 组成。其中 state 表示当前互斥锁的状态,而 sema 是用于控制锁状态的信号量。
1 | type Mutex struct { |
**state**:32位整数,按位存储锁的状态:
- 第0位:
locked(是否被锁定,0=未锁,1=已锁) - 第1位:
woken(是否有 goroutine 被唤醒,0=无,1=有) - 第2位:
starving(是否处于饥饿模式,0=正常,1=饥饿) - 剩余29位:
waiterCount(等待锁的 goroutine 数量)

在默认情况下,互斥锁的所有状态位都是 0
**sema**:信号量,用于阻塞和唤醒 goroutine(基于 runtime.semacquire 和 runtime.semrelease)
Lock() 加锁流程
(1) 快速路径(Fast Path)
如果锁未被占用(state == 0),直接通过 CAS(Compare-And-Swap) 获取锁:
1
2
3if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return // 成功获取锁
}适用于低竞争场景,几乎无开销。
(2) 慢路径(Slow Path)
如果锁已被占用,进入 自旋 + 阻塞 逻辑:
- 自旋尝试(Spin):
- 如果当前是 多核 CPU 且锁未进入 饥饿模式,goroutine 会自旋几次(约
4次),尝试抢锁。 - 自旋的目的是减少上下文切换开销。
- 如果当前是 多核 CPU 且锁未进入 饥饿模式,goroutine 会自旋几次(约
- 更新等待计数:
- 自旋失败后,通过
atomic.AddInt32(&m.state, 1<<mutexWaiterShift)增加waiterCount(等待者数量)。
- 自旋失败后,通过
- 进入阻塞:
- 调用
runtime.semacquire(&m.sema)让当前 goroutine 进入 阻塞状态,等待被唤醒。
- 调用
- 被唤醒后:
- 如果锁处于 饥饿模式,直接获取锁(避免新来的 goroutine 抢锁)。
- 否则,重新竞争锁。
Unlock() 解锁流程
1) 快速路径(Fast Path)
如果 无等待者(waiterCount == 0),直接释放锁:
1
2
3
4new := atomic.AddInt32(&m.state, -mutexLocked)
if new == 0 {
return // 无竞争,直接返回
}
(2) 慢路径(Slow Path)
如果有等待的 goroutine:
- 唤醒一个等待者:
- 如果锁未进入 饥饿模式,唤醒最早被阻塞的 goroutine,此时新来的goroutine可能直接抢到锁。
- 如果锁处于 饥饿模式,直接将锁交给 队首的等待者(避免新 goroutine 抢锁)。
- 减少等待计数:
- 更新
waiterCount并清除woken标志。
- 更新
正常模式和饥饿模式
正常模式
- 竞争激烈时性能优先:
- 在正常模式下,锁的获取是非公平的,新到达的 goroutine 可能会直接获取锁,而不需要等待。
- 自旋等待:
- 当锁被占用时,新到达的 goroutine 会自旋等待一段时间,尝试直接获取锁,而不是立即进入休眠状态。
- 适用场景:
- 适用于竞争不激烈或锁持有时间较短的场景,可以最大化性能。
饥饿模式
- 公平性优先:
- 在饥饿模式下,锁的获取是公平的,等待时间最长的 goroutine 会优先获取锁。
- 防止长时间等待:
- 当某个 goroutine 等待锁的时间超过一定阈值(默认为 1 毫秒),锁会进入饥饿模式。
- 适用场景:
- 适用于竞争激烈或锁持有时间较长的场景,可以防止某些 goroutine 长时间等

