1.介绍
本文通过阅读 Go 语言 sync.Mutex 的源码,我们一起学习 sync.Mutex 的底层实现。
2.sync.Mutex` 源码[1]分析
我们通过阅读 Go 语言 sync.Mutex 的源码,可以发现 sync.Mutex 结构体包含两个字段:
type Mutex struct {
state int32
sema uint32
}
- state:存储互斥锁的状态信息,包括锁是否被占用、是否进入饥饿模式,以及是否有协程被唤醒等。
- sema:信号量,用于阻塞和唤醒等待该互斥锁的协程。
state 字段
state 是一个 int32 类型的整数,用来表示互斥锁的当前状态。它并不是一个简单的布尔值,而是通过多个位(bit)来记录锁的不同状态。通过位运算,可以在同一个字段中存储多种状态信息。我们通过阅读 lockSlow() 方法的源码,可以发现 state 包含的几种状态。
state 主要包含以下几种状态:
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift
starvationThresholdNs = 1e6
)
- mutexLocker:表示互斥锁是否已被锁定。mutexLocked 是通过位移操作 1 << iota 定义的,当其值为 1 时,表示互斥锁已被锁定。
- mutexWoken:表示是否有等待的协程已被唤醒。这个状态位防止多个等待的协程被同时唤醒,从而避免竞争锁。
- mutexStarving:表示互斥锁是否处于饥饿模式。当一个等待的协程长时间无法获得锁(超过 1 毫秒),互斥锁会进入饥饿模式,此时锁的所有权会直接从释放锁的协程传递给等待队列中的下一个协程。
- mutexWaiterShift:表示有多少协程在等待获取互斥锁。通过将 state 字段右移来记录当前有多少协程处于等待状态(每个等待的协程增加一个值)。
推荐读者朋友们在项目开发中,多尝试使用位运算。
此外,我们通过阅读 lockSlow() 方法的源码,发现其内部实现中,使用“自旋锁”和“CAS”,分别是 runtime_doSpin() 和 atomic.CompareAndSwapInt32()。
使用“自旋锁”,当互斥锁可能很快被释放时,协程可能会短暂地自旋等待,从而减少 CPU 上下文切换的开销。
需要注意的是“自旋锁”会占用 CPU 资源,我们在项目开发中使用时,切勿长时间进行自旋等待。
使用“CAS”,用于对 state 变量进行原子更新,确保线程安全。
sema 字段
sema 是一个 uint32 类型的信号量,用来控制阻塞和唤醒等待互斥锁的协程。它通过与操作系统底层机制交互,负责在锁被占用时阻塞协程,当锁被释放时唤醒等待中的协程。
当一个协程尝试获取锁但锁已被占用时,它需要进入阻塞状态。 sema 字段与 Go 的 runtime(运行时)机制合作,将这些等待的协程挂起。当锁被释放时,runtime 会通过信号量来唤醒一个或多个等待的协程。
我们阅读 lockSlow() 方法和 unlockSlow() 方法的源码,可以发现 sema 通过 runtime_SemacquireMutex() 和 runtime_Semrelease() 函数进行操作。runtime_SemacquireMutex() 阻塞当前协程并等待信号量,runtime_Semrelease() 则负责释放信号量并唤醒等待的协程。
通过使用信号量,可以很好地处理高并发下的协程调度问题。与自旋锁不同,信号量机制不会占用 CPU 资源。当协程需要等待锁时,它可以通过信号量进入休眠,等待锁释放后再被唤醒,避免了忙等待带来的性能损耗。
3.总结
本文我们通过阅读 Go 语言 sync.Mutex 的源码,更加深入了解 sync.Mutex 的底层实现,它包含两种操作模式,分别是:
普通模式:
在普通模式下,等待的协程按 FIFO 顺序排队,但新到达的协程可以和被唤醒的协程竞争锁的所有权,因为新协程已经在 CPU 上运行,有一定的优势(即可以减少 CPU 上下文切换,从而提升性能)。如果一个协程等待超过 1 毫秒,互斥锁会切换到饥饿模式。
饥饿模式:
当协程等待超过 1 毫秒时,互斥锁进入饥饿模式。在饥饿模式下,新到达的协程不再直接尝试获取锁,而是排队等待。释放锁的协程会将锁直接交给队列中的第一个等待协程,从而避免长期等待的协程一直得不到锁的情况。
state 字段通过位操作存储了互斥锁的多种状态,包括是否锁定、是否进入饥饿模式、等待队列长度等,允许通过原子操作对这些状态进行高效的并发管理。
sema 字段是一个信号量,用于阻塞和唤醒等待锁的协程,结合 Go runtime 的机制,实现高效的协程调度和唤醒。
这两个字段共同构成了 sync.Mutex 的核心,保证了在高并发场景下的互斥锁操作既高效又安全。
随着 Go 语言版本迭代,sync.Mutex 的实现经过高度优化,能够在低竞争和高竞争场景中提供高效的锁定机制,同时尽量减少协程“饥饿”的情况。