单机锁实现原理

3889 字
19 分钟
单机锁实现原理

1. Sync.Mutex#

1.1 单机锁实现框架#

1.1.1 上锁/解锁#

通过Mutex内的一个状态值state来表示锁的状态:上锁把0变成1,解锁把1变成0。

上锁时假设state为0,使用原子操作将state从0变成1,如果成功则表示上锁成功。如果失败,说明state不为0,表示锁已经被其他协程持有,此时需要进入等待队列等待。

这是一个原子操作,保证了多个协程同时尝试上锁时不会出现竞态条件。

1.1.2 抢占锁的两种策略#

  • 阻塞/唤醒:当协程尝试上锁失败时,会将自己加入等待队列,并进入阻塞状态,直到被唤醒。这是悲观锁。
  • 自旋+CAS:协程在尝试上锁失败后,不立即阻塞,而是进行一段时间的忙等待(自旋),尝试再次获取锁。如果在自旋期间锁被释放,则可以直接获取锁,避免了阻塞和唤醒的开销。这是乐观锁。

正常模式下,Goroutine会先进行自旋4次尝试获取锁,如果自旋失败则进入阻塞状态。 饥饿模式下,Goroutine会直接进入阻塞状态,到阻塞队列末尾,不进行自旋。

需要注意的是,不管在哪种模式下,阻塞队列队头的Goroutine在被唤醒后都会得到不同程度的优待:在正常模式下,队头Goroutine会进行正常抢占锁,但是抢占失败依然会被放在队头;在饥饿模式下,队头Goroutine会直接获取锁。

1.1.3 饥饿模式#

饥饿模式:阻塞队列中的锁超过 1ms 而不得的Goroutine会被标记为饥饿,整个锁进入饥饿模式,将抢锁流程由非公平机制转为公平机制。饥饿模式下,保证FIFO,阻塞队列中的头部Goroutine获取锁,避免饥饿现象。

饥饿模式和正常模式的切换条件就是看阻塞队列中是否有Goroutine超过1ms而不得。

1.2 数据结构#

type Mutex struct {
state int32
sema uint32
}
  • state:表示锁的状态,包括是否被锁定、是否有等待的Goroutine、是否处于饥饿模式等信息。
  • sema:用于实现阻塞和唤醒机制的信号量。

1.2.1 几个全集变量#

const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
  • mutexLocked:表示锁被持有。
  • mutexWoken:表示有Goroutine被唤醒。
  • mutexStarving:表示锁处于饥饿模式。
  • mutexWaiterShift:表示等待Goroutine的数量在state中的偏移量。
  • starvationThresholdNs:表示饥饿模式的阈值,超过这个时间的Goroutine会被标记为饥饿。

1.2.2 state 字段详述#

state字段是一个32位整数,其中每一位或一组位表示不同的状态信息:

  • 第0位(mutexLocked):表示锁是否被持有。如果为1,表示锁被持有;如果为0,表示锁未被持有。
  • 第1位(mutexWoken):表示是否有Goroutine被唤醒。如果为1,表示有Goroutine被唤醒;如果为0,表示没有Goroutine被唤醒。
  • 第2位(mutexStarving):表示锁是否处于饥饿模式。如果为1,表示锁处于饥饿模式;如果为0,表示锁未处于饥饿模式。
  • 第3位及之后的位(mutexWaiterShift及之后的位):表示等待Goroutine的数量。通过将等待Goroutine的数量左移mutexWaiterShift位,可以将其存储在state字段的高位部分。最多可以表示 229−12^{29}-1 个等待Goroutine。

1.3 Mutex.Lock()#

1.3.1 Lock 方法主干#

func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}

首先尝试CAS获取锁,如果失败,说明锁已经被其他协程持有,进入慢路径lockSlow。这是在正常模式下的上锁流程,因为atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)保证了state为0,一定不是饥饿模式。

1.3.2 Mutex.lockSlow()#

1.3.2.1 几个局部变量#

func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// ...
}
}
  • waitStartTime:记录Goroutine开始等待的时间,用于判断是否进入饥饿模式。
  • starving:表示当前Goroutine是否处于饥饿状态。
  • awoke:表示当前Goroutine是否被唤醒。
  • iter:表示当前Goroutine尝试获取锁的次数,用于控制自旋次数。
  • old:记录当前的state值,用于后续的CAS操作。

1.3.2.2 自旋空转#

func (m *Mutex) lockSlow() {
// ...
for {
// 进入该 if 分支,说明抢锁失败,处于正常模式,且仍满足自旋条件
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 进入自旋后处理环节
// ...
continue
}
// ...
}
}
  • 假如满足三个条件:1. 当前锁处于正常模式 2. 锁被持有 3. 自旋次数未达上限,则进行自旋。则进入自旋后处理环节。
  • 进入自旋后处理环节中,假如当前锁有尚未唤醒的阻塞协程,则通过 CAS 操作将 state 的 mutexWoken 标识置为 1,将局部变量 awoke 置为 true。
  • 调用 runtime_doSpin 告知调度器 P 当前处于自旋模式。
  • 更新自旋次数 iter 和锁状态值 old。
  • 通过 continue 语句进入下一轮尝试。

1.3.2.3 state 新值构造#

func (m *Mutex) lockSlow() {
// ...
for {
// 自旋抢锁失败后处理 ...
new := old
if old&mutexStarving == 0 {
new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
new &^= mutexWoken
}
// ...
}
}
  • 从自旋中走出来后,会存在两种分支,要么加锁成功,要么陷入自锁,不论是何种情形,都会先对 sync.Mutex 的状态新值 new 进行更新;
  • 倘若当前是非饥饿模式,则在新值 new 中置为已加锁,即尝试抢锁;
  • 倘若旧值为已加锁或者处于饥饿模式,则当前 goroutine 在这一轮注定无法抢锁成功,可以直接令新值的阻塞协程数加1;
  • 倘若当前进入饥饿模式且旧值已加锁,则将新值置为饥饿模式;
  • 倘若局部变量标识是已有唤醒协程抢锁,说明 Mutex.state 中的 mutexWoken 是被当前 goroutine 置为 1 的,但由于当前 goroutine 接下来要么抢锁成功,要么被阻塞挂起,因此需要在新值中将该 mutexWoken 标识更新置 0.

1.3.2.4 state 新旧值替换#

func (m *Mutex) lockSlow() {
// ...
for {
// 自旋抢锁失败后处理 ...
// new/old 状态值更新 ...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// case1 加锁成功
// case2 将当前 goroutine 挂起
// ...
} else {
old = m.state
}
// ...
}
}
  • 通过 CAS 操作,用构造的新值替换旧值;
  • 倘若失败(即旧值被其他 goroutine 介入提前修改导致不符合预期),则将旧值更新为此刻的 Mutex.state,并开启一轮新的循环;
  • 倘若 CAS 替换成功,则进入最后一轮的二择一局面:
    • I)倘若当前 goroutine 加锁成功,则返回;
    • II)倘若失败,则将 goroutine 挂起并添加到阻塞队列。

1.3.2.5 上锁成功分支#

加锁成功的分支

func (m *Mutex) lockSlow() {
// ...
for {
// 自旋抢锁失败后处理 ...
// new/old 状态值更新 ...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// ...
}
// ...
}
}
  • 延续 1.3.2.4 的思路,此时已经成功将 Mutex.state 由旧值替换为新值;
  • 接下来进行判断:倘若旧值是未加锁状态且为正常模式,则意味着加锁标识位正是由当前 goroutine 完成的更新,说明加锁成功,返回即可;
  • 倘若旧值中锁未释放或者处于饥饿模式,则当前 goroutine 需要进入阻塞队列挂起。

1.3.2.6 阻塞挂起#

阻塞挂起 goroutine

func (m *Mutex) lockSlow() {
// ...
for {
// 自旋抢锁失败后处理 ...
// new/old 状态值更新 ...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 加锁成功后返回的逻辑分支 ...
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// ...
}
// ...
}
}

承接上节,走到此处的情形有两种:要么是抢锁失败,要么是锁已处于饥饿模式,而当前 goroutine 不是从阻塞队列被唤起的协程。不论处于哪种情形,当前 goroutine 都面临被阻塞挂起的命运。

  • 基于 queueLifo 标识当前 goroutine 是从阻塞队列被唤起的“老客”还是新进流程的“新客”;
  • 倘若等待的起始时间为 0,则为新客;倘若非 0,则为老客;
  • 倘若是新客,则对等待的起始时间进行更新,置为当前时刻的 ns 时间戳;
  • 将当前 goroutine 添加到阻塞队列中:倘若是老客则挂入队头;倘若是新客则挂入队尾;
  • 挂起当前 goroutine。

1.3.2.7 从阻塞态被唤醒#

goroutine 被唤醒后

func (m *Mutex) lockSlow() {
// ...
for {
// 自旋抢锁失败后处理...
// new/old 状态值更新 ...
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 加锁成功后返回的逻辑分支 ...
// 挂起前处理 ...
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 从阻塞队列被唤醒了
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
}
// ...
}
}
  • 走入此处,说明当前 goroutine 是从 Mutex 的阻塞队列中被唤起的;
  • 判断是否需要进入饥饿模式:倘若当前 goroutine 在阻塞队列中等待时间长达 1ms,则更新 starving 变量,并在下一轮循环中完成对 Mutex.state 中 mutexStarving 标识位的更新;
  • 获取此时锁的状态,通过 old 存储;
  • 倘若此时锁是饥饿模式,则当前 goroutine 无需竞争可以直接获得锁;
  • 饥饿模式下,goroutine 获取锁前需要更新锁的状态,包含 mutexLocked、阻塞队列等待 goroutine 数以及 mutexStarving 三个信息;均通过 delta 变量记录差值,最终通过原子操作添加到 Mutex.state 中;
  • mutexStarving 的更新要作前置判断:倘若当前 starving 为 false,或者当前 goroutine 就是阻塞队列的最后一个 goroutine,则将 Mutex.state 置为正常模式。

1.4 Unlock#

1.4.1 Unlock 方法主干#

Unlock方法主流程

func (m *Mutex) Unlock() {
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}
  • 通过原子操作解锁;
  • 倘若解锁时发现,目前参与竞争的仅有自身一个 goroutine,则直接返回即可;
  • 倘若发现锁中还有阻塞协程,则走入 unlockSlow 分支。

1.4.2 unlockSlow#

解锁时唤醒等锁的阻塞 goroutine

1.4.2.1 未加锁的异常情形#

func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
// ...
}

解锁时倘若发现 Mutex 此前未加锁,直接抛出 fatal。

1.4.2.2 正常模式#

func (m *Mutex) unlockSlow(new int32) {
// ...
if new&mutexStarving == 0 {
old := new
for {
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
}
// ...
}
  • 倘若阻塞队列内无 goroutine 或者 mutexLocked、mutexStarving、mutexWoken 标识位任一不为零,三者均说明此时有其他活跃协程已介入,自身无需关心后续流程;
  • 基于 CAS 操作将 Mutex.state 中的阻塞协程数减 1,倘若成功,则唤起阻塞队列头部的 goroutine,并退出;
  • 倘若减少阻塞协程数的 CAS 操作失败,则更新此时的 Mutex.state 为新的 old 值,开启下一轮循环。

1.4.2.3 饥饿模式#

func (m *Mutex) unlockSlow(new int32) {
// ...
if new&mutexStarving == 0 {
// ...
} else {
runtime_Semrelease(&m.sema, true, 1)
}
}

饥饿模式下,直接唤醒阻塞队列头部的 goroutine 即可。

2. Sync.RWMutex#

2.1 核心机制#

  • 从逻辑上,可以把 RWMutex 理解为一把读锁加一把写锁;
  • 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的 goroutine 均阻塞;
  • 读锁具有有限的共享性,当其被占用,试图取写锁的 goroutine 会阻塞,试图取读锁的 goroutine 可与当前 goroutine 共享读锁;
  • 综上可见,RWMutex 适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则 RWMutex 退化为普通的 Mutex。

2.2 数据结构#

RWLock数据结构

const rwmutexMaxReaders = 1 << 30
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
  • rwmutexMaxReaders:共享读锁的 goroutine 数量上限,值为 2292^{29};
  • w:RWMutex 内置的一把普通互斥锁 sync.Mutex;
  • writerSem:关联写锁阻塞队列的信号量;
  • readerSem:关联读锁阻塞队列的信号量;
  • readerCount:正常情况下等于介入读锁流程的 goroutine 数量;当 goroutine 接入写锁流程时,该值为实际介入读锁流程的 goroutine 数量减 rwmutexMaxReaders;
  • readerWait:记录在当前 goroutine 获取写锁前,还需要等待多少个 goroutine 释放读锁。

2.3 读锁流程#

2.3.1 RLock#

func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
  • 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数加一;
  • 倘若 RWMutex.readCount 的新值仍小于 0,说明有 goroutine 未释放写锁,因此将当前 goroutine 添加到读锁的阻塞队列中并阻塞挂起。

2.3.2 RUnlock#

2.3.2.1 RUnlock 方法主干#

func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
rw.rUnlockSlow(r)
}
}
  • 基于原子操作,将 RWMutex 的 readCount 变量加一,表示占用或等待读锁的 goroutine 数减一;
  • 倘若 RWMutex.readCount 的新值小于 0,说明有 goroutine 在等待获取写锁,则走入 RWMutex.rUnlockSlow 的流程中。

2.3.2.2 rUnlockSlow#

func (rw *RWMutex) rUnlockSlow(r int32) {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
fatal("sync: RUnlock of unlocked RWMutex")
}
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
  • 对 RWMutex.readerCount 进行校验,倘若发现当前协程此前未抢占过读锁,或者介入读锁流程的 goroutine 数量达到上限,则抛出 fatal;
  • (倘若 r+1 == -rwmutexMaxReaders,说明此时有 goroutine 介入写锁流程,但当前此前未加过读锁,具体原因见 2.3 小节;倘若 r+1==0,则要么此前未加过读锁,要么介入读锁流程的 goroutine 数量达到上限,具体原因见 2.3 小节。)
  • 基于原子操作,对 RWMutex.readerWait 进行减一操作,倘若其新值为 0,说明当前 goroutine 是最后一个介入读锁流程的协程,因此需要唤醒一个等待写锁的阻塞队列的 goroutine。(综合 RWMutex.readerCount 为负值,可以确定存在等待写锁的 goroutine,具体原因见 2.3 小节。)

2.4 写锁流程#

2.4.1 Lock#

func (rw *RWMutex) Lock() {
rw.w.Lock()
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
  • 对 RWMutex 内置的互斥锁进行加锁操作;
  • 基于原子操作,对 RWMutex.readerCount 进行减少 -rwmutexMaxReaders 的操作;
  • 倘若此时存在未释放读锁的 gouroutine,则基于原子操作在 RWMutex.readerWait 的基础上加上介入读锁流程的 goroutine 数量,并将当前 goroutine 添加到写锁的阻塞队列中挂起。

2.4.2 Unlock#

func (rw *RWMutex) Unlock() {
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
fatal("sync: Unlock of unlocked RWMutex")
}
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
rw.w.Unlock()
}
  • 基于原子操作,将 RWMutex.readerCount 的值加上 rwmutexMaxReaders;
  • 倘若发现 RWMutex.readerCount 的新值大于 rwmutexMaxReaders,则说明要么当前 RWMutex 未上过写锁,要么介入读锁流程的 goroutine 数量已经超限,因此直接抛出 fatal;
  • 因此唤醒读锁阻塞队列中的所有 goroutine;(可见,竞争读锁的 goroutine 更具备优势)
  • 解开 RWMutex 内置的互斥锁。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

单机锁实现原理
https://www.lansganbs.cn/posts/go源码/单机锁实现原理/
作者
Zowely
发布于
2026-01-18
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zowely
红叶最多情,一舞寄相思。
公告
欢迎来到我的博客!这里分享计算机等相关内容。