乐于分享
好东西不私藏

Go sync.RWMutex 源码深度解析

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 = 一个绝对值很大的负数

这实现了两个效果:

  1. 标记作用:后续读者看到负数,就知道有 writer 在等待,自己必须阻塞
  2. 恢复作用: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, false0)    }// 如果 >=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, false1)    }}

关键逻辑:

┌─────────────────┐│   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, false0)    }}

阶段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, false0)}

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, false0)    }// 【阶段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, false0)}

阶段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 获得 │          │                   │                │ 写锁成功    │          │                   │                └────────────┘

流程说明:

  1. T1-T2:Reader1、Reader2 先后获得读锁,readerCount = 2
  2. T3-T5:Writer 调用 Lock(),获取 w 锁,将 readerCount 减 MaxReaders 标记等待状态,readerWait = 2
  3. T6:Writer 阻塞在 writerSem,等待读者退出
  4. T7:Reader1 RUnlock,readerWait = 1,Writer 继续等待
  5. 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  │                        │ 完成     │      │ 完成     │                        └──────────┘      └──────────┘

流程说明:

  1. T1:Writer 获取锁,readerCount = -MaxReaders(负数标记)
  2. T2-T3:Reader3、Reader4 先后尝试 RLock(),都看到负数,阻塞在 readerSem
  3. T4:Writer 执行写操作
  4. T5:Writer 调用 Unlock(),Add(+MaxReaders) 返回 2(表示有 2 个读者在等待)
  5. T6-T7:循环两次调用 Semrelease,依次唤醒 Reader3 和 Reader4
  6. 最后 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 使用建议

场景
建议
读多写少
RWMutex 是正确选择
读写均衡
考虑普通 Mutex,更简单
写多读少
不要用 RWMutex,Mutex 更好
需要锁升级
不建议,会死锁
递归读锁
不建议,会死锁