Go sync.RWMutex 源码深度解析

目录
-
引言 -
一、核心数据结构 -
1.1 RWMutex 结构体 -
1.2 rwmutexMaxReaders 常量 -
二、读锁流程(RLock / RUnlock) -
2.1 RLock 快速路径 -
2.2 RUnlock 快速路径 -
2.3 rUnlockSlow 慢速路径 -
三、写锁流程(Lock / Unlock) -
3.1 Lock 三阶段 -
3.2 Unlock 三阶段 -
四、完整的加锁解锁流程 -
五、TryLock / TryRLock -
5.1 TryRLock 实现 -
5.2 TryLock 实现 -
5.3 适用场景 -
六、死锁场景 -
6.1 不能锁升级/降级 -
6.2 递归读锁死锁 -
七、总结 -
7.1 核心机制回顾 -
7.2 回答引言中的问题 -
7.3 使用建议
引言
要理解 sync.RWMutex,先考虑这几个问题:
-
读锁怎么实现并发? 多个 goroutine 同时读,用什么机制避免互斥? -
写锁之间怎么互斥? 多个 writer 竞争时,谁先谁后? -
写锁和读锁怎么互斥? 写的时候不能读,读的时候不能写,怎么协调? -
怎么防止写锁饥饿? 如果读者源源不断,writer 会不会永远等待?
本文从源码层面解答这些问题。源码基于 Go 1.24.3
要理解加锁解锁的流程,首先得搞清楚 RWMutex 内部的数据结构
一、核心数据结构
1.1 RWMutex 结构体
type RWMutex struct { w Mutex // 互斥锁:用于 writer 之间的互斥竞争 writerSem uint32// 写者信号量:writer 等待读者完成时使用 readerSem uint32// 读者信号量:读者等待 writer 完成时使用 readerCount atomic.Int32 // 【核心】读者计数器:正数=当前读者数;负数=有writer等待 readerWait atomic.Int32 // 【核心】等待退出的读者数:writer需要等多少个读者退出}
5 个字段分工如下:
┌─────────────────────────────────────────────────────────────┐│ RWMutex 结构图解 │├─────────────────────────────────────────────────────────────┤│ ││ ┌───────────┐ ┌───────────────┐ ││ │ w │────▶│ Mutex │ writer 间互斥 ││ │ (Mutex) │ │ │ ││ └───────────┘ └───────────────┘ ││ ││ ┌───────────┐ ┌───────────────────────────┐ ││ │ readerCount│ │ 正数: 当前读者数 │ ││ │ (atomic) │────▶│ 负数: 有 writer 等待 │ ││ └───────────┘ └───────────────────────────┘ ││ ││ ┌───────────┐ ┌───────────────────────────┐ ││ │ readerWait│ │ writer 需要等待的读者数 │ ││ │ (atomic) │ │ 最后一个读者负责唤醒 writer│ ││ └───────────┘ └───────────────────────────┘ ││ ││ ┌───────────┐ ┌───────────────────────────┐ ││ │ writerSem │ │ writer 阻塞的信号量 │ ││ │ (uint32) │ │ 被读者唤醒 │ ││ └───────────┘ └───────────────────────────┘ ││ ││ ┌───────────┐ ┌───────────────────────────┐ ││ │ readerSem │ │ 读者阻塞的信号量 │ ││ │ (uint32) │ │ 被 writer 唤醒 │ ││ └───────────┘ └───────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────┘
1.2 rwmutexMaxReaders 常量
const rwmutexMaxReaders = 1 << 30// 约 10 亿
这个常量用来实现”负数标记法”——把 readerCount 从正数变成负数,表示有 writer 在等待。
Go 用了一个技巧:rwmutexMaxReaders 是 2^30(约 10 亿),足够大。当 writer 获取锁时:
readerCount 原本是 N(当前读者数)writer 执行 Add(-rwmutexMaxReaders) 后: readerCount = N - 2^30 = 一个绝对值很大的负数
这实现了两个效果:
-
标记作用:后续读者看到负数,就知道有 writer 在等待,自己必须阻塞 -
恢复作用:writer 释放时 Add(+rwmutexMaxReaders),负数变回 N,读者数仍然正确
简单说:readerCount 的正负就是”有没有 writer”的开关。
了解了数据结构,下面来看具体的加锁解锁流程。先看读锁的实现
二、读锁流程(RLock / RUnlock)
2.1 RLock 快速路径
func(rw *RWMutex)RLock() {// 【关键】Add(1) 返回增加后的值。如果为负,说明有 writer 正在等待if rw.readerCount.Add(1) < 0 {// 有 writer 正在等待,当前读者需要阻塞,等待 writer 完成 runtime_SemacquireRWMutexR(&rw.readerSem, false, 0) }// 如果 >=0,快速路径成功,直接获得读锁}
流程图:
┌─────────────────┐│ RLock() │└────────┬────────┘ │ ▼┌─────────────────┐│ Add(1) 原子操作 │◀─────────────────────────┐└────────┬────────┘ │ │ │ ┌────┴────┐ │ │ 结果 < 0?│ │ └────┬────┘ │ Yes│ No │ ┌────┘ └──────────┐ │ ▼ ▼ │┌─────────────┐ ┌───────────────┐ ││ 有 writer │ │ 快速路径成功 │ ││ 正在等待? │ │ 直接返回 │ │└──────┬──────┘ └───────────────┘ │ │ │ ▼ │┌─────────────┐ ││ 阻塞在 │ ││ readerSem │───────────────────────────────┘│ 等待唤醒 │ (被 writer Unlock 唤醒)└─────────────┘
2.2 RUnlock 快速路径
func(rw *RWMutex)RUnlock() {// 【关键】Add(-1) 返回增加后的值// 如果 r < 0,说明有 writer 在等待,进入慢路径处理唤醒逻辑// 如果 r >= 0,快速路径成功,直接返回if r := rw.readerCount.Add(-1); r < 0 { rw.rUnlockSlow(r) }}
2.3 rUnlockSlow 慢速路径
func(rw *RWMutex)rUnlockSlow(r int32) {// 【核心】有 writer 正在等待,递减 readerWait 计数if rw.readerWait.Add(-1) == 0 {// 【关键】最后一个活跃的读者负责唤醒 writer runtime_Semrelease(&rw.writerSem, false, 1) }}
关键逻辑:
┌─────────────────┐│ RUnlock() │└────────┬────────┘ │ ▼┌─────────────────┐│ Add(-1) 原子操作 │└────────┬────────┘ │ ┌────┴────┐ │ 结果 < 0?│ └────┬────┘ Yes│ No ┌────┘ └──────────┐ ▼ ▼┌─────────────┐ ┌───────────────┐│ 进入慢路径 │ │ 快速路径成功 ││ rUnlockSlow │ │ 直接返回 │└──────┬──────┘ └───────────────┘ │ ▼┌─────────────┐│ readerWait ││ Add(-1) │└──────┬──────┘ │ ┌──┴──┐ │ == 0?│ ━━━ 是最后一个读者? └──┬──┘ Yes│ No ┌──┘ └──┐ ▼ ▼┌─────────┐ ┌──────────┐│ 唤醒 │ │ 直接返回 ││ writer │ │ │└─────────┘ └──────────┘
读锁逻辑相对简单,写锁要处理与读者的协调,复杂度更高。下面详细分析
三、写锁流程(Lock / Unlock)
3.1 Lock 三阶段
func(rw *RWMutex)Lock() {// 【阶段1】先获取 w 锁,解决与其他 writer 的竞争(writer 之间互斥) rw.w.Lock()// 【阶段2】标记 writer 等待状态,同时获取当前活跃读者数// Add(-MaxReaders) 使 readerCount 变负,阻塞新读者// 返回值是增加前的读者数 N,+MaxReaders 后 r = N(原读者数) r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders// 【阶段3】如果有活跃读者,等待它们全部退出// readerWait.Add(r) 返回增加后的值,如果为0说明没有读者需要等if r != 0 && rw.readerWait.Add(r) != 0 {// 阻塞在 writerSem,等待最后一个读者唤醒 runtime_SemacquireRWMutex(&rw.writerSem, false, 0) }}
阶段1:获取 w(互斥其他 writer)
┌─────────────────┐│ Lock() │└────────┬────────┘ │ ▼┌─────────────────┐│ rw.w.Lock() │◀──── 确保只有一个 writer 能进入└────────┬────────┘
阶段2:标记并获取当前读者数
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
这行代码的技巧:
-
Add(-rwmutexMaxReaders)使readerCount变成负数,阻塞新读者 -
返回值是增加前的值(即原读者数 N) -
+ rwmutexMaxReaders抵消后,r = 原读者数 N
阶段3:等待活跃读者退出
if r != 0 && rw.readerWait.Add(r) != 0 { runtime_SemacquireRWMutex(&rw.writerSem, false, 0)}
3.2 Unlock 三阶段
func(rw *RWMutex)Unlock() {// 【阶段1】恢复 readerCount,从负数变回正数(或0)// r 是等待中的读者数量(在 writer 持有锁期间被阻塞的读者) r := rw.readerCount.Add(rwmutexMaxReaders)// 【阶段2】唤醒所有在 writer 期间被阻塞的读者// 每个被唤醒的读者会从 RLock() 的慢路径继续执行for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false, 0) }// 【阶段3】释放 w 锁,允许其他 writer 竞争 rw.w.Unlock()}
阶段1:恢复读者计数
r := rw.readerCount.Add(rwmutexMaxReaders)
-
将负数恢复为正数(或 0) -
r是被阻塞的读者数(因为在 writer 持有锁期间,尝试获取读锁的读者会阻塞在 readerSem)
阶段2:唤醒所有阻塞读者
for i := 0; i < int(r); i++ { runtime_Semrelease(&rw.readerSem, false, 0)}
阶段3:释放 writer 互斥锁
rw.w.Unlock()
上面分别介绍了读锁和写锁的流程,但读者和写者如何协调还不太直观。下面通过一个完整的时间线,展示三者之间的交互过程
四、完整的加锁解锁流程
reader 释放锁唤醒writer
时间 Reader 1 Reader 2 Writer───────────────────────────────────────────────────────────────── │ │ │ T1 ▼ │ │ ┌─────────┐ │ │ │ RLock() │ │ │ │ 成功 │ │ │ │ rc=1 │ │ │ └────┬────┘ │ │ │ │ │ T2 │ ▼ │ │ ┌─────────┐ │ │ │ RLock() │ │ │ │ 成功 │ │ │ │ rc=2 │ │ │ └────┬────┘ │ │ │ │ T3 │ │ ▼ │ │ ┌─────────┐ │ │ │ Lock() │ │ │ │w.Lock() │ │ │ │ 成功 │ │ │ └────┬────┘ │ │ │ T4 │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ Add(-MaxReaders) │ │ │ │ rc = 2 - MaxReaders │ │ │ │ 返回 r = 2 │ │ │ └──────────┬──────────┘ │ │ │ T5 │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ readerWait.Add(2) │ │ │ │ 返回 2 (不为0) │ │ │ └──────────┬──────────┘ │ │ │ T6 │ │ ▼ │ │ ┌─────────────────────┐ │ │ │ 阻塞在 writerSem │ │ │ │ 等待读者唤醒... │ │ │ └─────────────────────┘ │ │ ▲ T7 ▼ │ │ ┌─────────┐ │ │ │RUnlock()│ │ │ │Add(-1) │ │ │ │rw=1 │──────────────┼───────────────────┘ └────┬────┘ │ 还没唤醒,继续等 │ │ T8 │ ▼ │ ┌─────────┐ │ │RUnlock()│ │ │Add(-1) │ │ │rw=0 │───────────────────┐ │ └────┬────┘ │ │ │ │ │ │ 最后一个读者唤醒 writer T9 │ │ │ │ │ ▼ │ │ ┌────────────┐ │ │ │ Writer 获得 │ │ │ │ 写锁成功 │ │ │ └────────────┘
流程说明:
-
T1-T2:Reader1、Reader2 先后获得读锁,readerCount = 2 -
T3-T5:Writer 调用 Lock(),获取 w 锁,将 readerCount 减 MaxReaders 标记等待状态,readerWait = 2 -
T6:Writer 阻塞在 writerSem,等待读者退出 -
T7:Reader1 RUnlock,readerWait = 1,Writer 继续等待 -
T8-T9:Reader2 RUnlock,readerWait = 0,唤醒 Writer,Writer 获得写锁
下面再看 writer 释放锁时如何唤醒被阻塞的读者。
Writer 释放锁唤醒读者
时间 Writer Reader 3 Reader 4───────────────────────────────────────────────────────────────── │ │ │ T1 │ │ │ ▼ │ │ ┌─────────┐ │ │ │ Lock() │ │ │ │ 成功 │ │ │ │ rc=-M │ │ │ └────┬────┘ │ │ │ ▼ │ │ ┌─────────┐ │ T2 │ │ RLock() │ │ │ │ 看到负数│ │ │ │ 阻塞中..│ ▼ │ └─────────┘ ┌─────────┐ │ . │ RLock() │ T3 │ . │ 看到负数│ │ . │ 阻塞中..│ │ . └─────────┘ T4 │ . . ┌────┴────────┐ . . │ 执行写操作 │ . . │ rc 保持负数 │ . . └─────────────┘ . . │ . . T5 ▼ . . ┌─────────┐ . . │Unlock() │ . . │Add(+M)=2│ . . │(2个等待) │ . . └────┬────┘ . . │ . . │ 唤醒Reader3 . . T6 │──────▶───────────┼─────────────────────┤ │ ┌─────────┐ │ │ │继续执行 │ │ │ └─────────┘ │ │ │ │ 唤醒Reader4 │ T7 │──────────────────────▶───────────────┼ │ │ ┌─────────┐ │ │ │继续执行 │ ▼ │ └─────────┘ ┌─────────┐ │ │ │w.Unlock() │ │ │ 完成 │ │ │ └─────────┘ ▼ ▼ ┌──────────┐ ┌──────────┐ │ RUnlock │ │ RUnlock │ │ 完成 │ │ 完成 │ └──────────┘ └──────────┘
流程说明:
-
T1:Writer 获取锁,readerCount = -MaxReaders(负数标记) -
T2-T3:Reader3、Reader4 先后尝试 RLock(),都看到负数,阻塞在 readerSem -
T4:Writer 执行写操作 -
T5:Writer 调用 Unlock(),Add(+MaxReaders) 返回 2(表示有 2 个读者在等待) -
T6-T7:循环两次调用 Semrelease,依次唤醒 Reader3 和 Reader4 -
最后 Writer 释放 w 锁,Reader3 和 Reader4 继续执行
五、TryLock / TryRLock
除了常规的阻塞式加锁,RWMutex 还提供了非阻塞的 TryLock 方法
5.1 TryRLock 实现
func(rw *RWMutex)TryRLock()bool {for {// 【检查】读取当前读者数 c := rw.readerCount.Load()// 【关键】如果为负,说明有 writer 正在等待,直接失败if c < 0 {returnfalse }// 【尝试】CAS 增加读者数,成功则获得读锁if rw.readerCount.CompareAndSwap(c, c+1) {returntrue }// CAS 失败,说明有其他读者并发修改,重试 }}
5.2 TryLock 实现
func(rw *RWMutex)TryLock()bool {// 【阶段1】尝试获取 writer 互斥锁,失败说明有其他 writerif !rw.w.TryLock() {returnfalse// 其他 writer 正在竞争或持有锁 }// 【阶段2】尝试将 readerCount 从 0 改为 -MaxReaders// 只有 readerCount == 0 时才成功(没有活跃读者)if !rw.readerCount.CompareAndSwap(0, -rwmutexMaxReaders) {// 【失败回滚】有活跃读者,释放 w 锁并返回失败 rw.w.Unlock()returnfalse }// 【成功】获得写锁,此时 readerCount 为负,阻塞新读者returntrue}
5.3 适用场景
场景1:获取不到就算了,最佳努力缓存更新
// 定期清理过期缓存,如果锁竞争失败就跳过本次,等下一轮func(c *Cache)cleanup() {// 不需要阻塞等待,这次清理不了还有下次if !c.rw.TryLock() {return// 有其他 writer 或 reader,跳过本次清理 }defer c.rw.Unlock()// 执行清理...}
场景2:多锁获取的死锁避免(带超时/回退)
// 需要同时获取多个锁,用 TryLock 避免不同的加锁顺序导致的死锁functransfer(a, b *Account, amount int)error {// 先尝试获取两个锁if !a.mu.TryLock() {return errors.New("无法获取 a 的锁") }defer a.mu.Unlock()if !b.mu.TryLock() {return errors.New("无法获取 b 的锁") }defer b.mu.Unlock()// 执行转账...}
为什么不用 TryLock 会死锁?
场景:G1 转账 A→B,G2 转账 B→A用 Lock() 固定顺序的问题:G1: Lock(a) 成功 ──▶ 尝试 Lock(b) ──▶ 阻塞(b 被 G2 持有)G2: Lock(b) 成功 ──▶ 尝试 Lock(a) ──▶ 阻塞(a 被 G1 持有)死锁:G1 等 b,G2 等 a,互相等待
用 TryLock 为什么不会死锁?
G1: TryLock(a) 成功 ──▶ TryLock(b) 失败 ──▶ 释放 a,返回错误G2: TryLock(b) 成功 ──▶ TryLock(a) 失败 ──▶ 释放 b,返回错误结果:双方都失败,但没有死锁,可以重试
注意:这个例子是简化版。实际生产环境应该用固定加锁顺序(如按地址排序)来避免死锁,而不是依赖 TryLock
场景3:乐观更新(计算后尝试写入)
// 先读后算再写,但如果写的时候数据已被修改,就放弃本次结果func(s *Server)tryOptimize() { s.mu.RLock() data := s.copyData() // 读取当前数据 s.mu.RUnlock()// 在后台计算优化结果(可能耗时较长) optimized := optimize(data)// 尝试获取写锁应用结果if !s.mu.TryLock() {// 【关键】获取失败说明有其他 writer 正在修改// 此时 optimized 是基于旧数据计算的,可能已经过时// 放弃本次结果,下次重新计算,避免覆盖别人的更新return }defer s.mu.Unlock() s.data = optimized}
为什么必须用 TryLock?
如果用 Lock() 会阻塞等待,问题在于:
-
计算 optimized是基于旧的 data -
阻塞期间,其他 writer 可能已经修改了数据 -
等获得锁时, optimized已经过时,覆盖新数据会造成更新丢失
用 TryLock() 获取不到就放弃:
-
不阻塞,不浪费 CPU -
避免用过时数据覆盖新数据 -
下次重新读取、重新计算、重新尝试
这是一种乐观锁思想:先计算,再尝试提交,冲突就重试
六、死锁场景
下面看几个典型的错误用法导致的死锁场景
6.1 不能锁升级/降级
// 错误示例:锁升级导致死锁var rw sync.RWMutexfuncbuggy() { rw.RLock()defer rw.RUnlock()// 需要修改数据,尝试升级... rw.Lock() // 死锁!自己持有的 RLock 阻止了 Lockdefer rw.Unlock()}
死锁原因:
G1 持有读锁 → 尝试获取写锁 ↓ 写锁检查到有读者(G1自己) ↓ 写锁等待所有读者退出 ↓ 但 G1 的读锁要在函数结束才释放 ↓ 死锁:写锁等读锁,读锁等函数结束,函数结束要等写锁成功
6.2 有递归读锁死锁
// 错误示例:递归读锁导致死锁func(s *Server)Handler() { s.rw.RLock()defer s.rw.RUnlock() s.process() // 内部又调用了 RLock()!}func(s *Server)process() {// 【死锁前提】此时已有 Writer 在等待(readerCount 为负数)// 虽然 G1 已持有读锁,但第二次 RLock 看到负数会阻塞// 因为 Writer 正在等待所有读者退出,包括 G1 自己 s.rw.RLock()defer s.rw.RUnlock()}
死锁条件:
前提:已有 Writer 在等待锁(readerCount 为负数)G1 第一次 RLock() 成功(在 writer 标记之前) ↓ Writer 调用 Lock(),readerCount 变负 ↓ G1 第二次调用 RLock(),看到负数 ↓ G1 阻塞在 readerSem 等待 Writer 完成 ↓ 但 Writer 又在等 G1 释放第一次的读锁 ↓ 死锁:G1 等 Writer,Writer 等 G1
七、总结
最后,我们回顾整个 RWMutex 的核心机制,并回答引言中提出的问题
7.1 核心机制回顾
┌─────────────────────────────────────────────────────────────┐│ RWMutex 核心机制 │├─────────────────────────────────────────────────────────────┤│ ││ 1. readerCount 的双重语义 ││ ┌─────────────┐ ┌─────────────┐ ││ │ 正数/零 │ │ 负数 │ ││ │ 读者数 │◀──────▶│ Writer 等待 │ ││ └─────────────┘ └─────────────┘ ││ ││ 2. Writer 获取锁的三阶段 ││ w.Lock() ──▶ Add(-MaxReaders) ──▶ 等待 readerWait ││ ││ 3. 读者与 Writer 的协调 ││ • Writer 通过 readerCount < 0 标记,阻塞新读者 ││ • 读者通过 readerWait 计数,最后一个唤醒 Writer ││ • 信号量(readerSem/writerSem)实现阻塞/唤醒 ││ │└─────────────────────────────────────────────────────────────┘
7.2 回答引言中的问题
问题1:读锁怎么实现并发?
用原子操作 readerCount.Add(1)
-
没 writer 时(readerCount >= 0),读者只做一个原子加法就获得锁 -
多个读者并发执行 Add(1),各自成功,自然实现并发 -
不需要像 Mutex 那样竞争锁状态
问题2:写锁之间怎么互斥?
用内嵌的 w Mutex
-
所有 writer 先竞争这个 Mutex,胜者才能进入后续流程 -
确保同一时刻只有一个 writer 能修改 readerCount、等待读者退出 -
其他 writer 在 w.Lock()处阻塞
问题3:写锁和读锁怎么互斥?
用 readerCount 的正负标记:
-
写锁获取: Add(-MaxReaders)把 readerCount 变负数 -
新读者看到负数,就知道有 writer 在等待,自己阻塞在 readerSem -
写锁释放: Add(+MaxReaders)恢复正数,唤醒阻塞的读者
问题4:怎么防止写锁饥饿?
关键设计:writer 一旦开始等待,后续读者一律阻塞
-
Writer 在 Add(-MaxReaders)后,readerCount 变负 -
新读者的 Add(1)返回负数,直接阻塞,不能插队 -
现有读者退出后,writer 获得锁,完成后再唤醒等待的读者 -
没有强公平保证,但不会无限期饥饿
7.3 使用建议
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
夜雨聆风