单机锁实现原理
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字段的高位部分。最多可以表示 个等待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 数量上限,值为 ;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内置的互斥锁。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


