Golang读写锁底层实现揭秘‌

概述

在并发编程中,合理使用读写锁可以大幅度提升程序的性能,尤其是“读多写少”的场景。Go 标准库提供了 sync.RWMutex,它允许多个读操作并发执行,但写操作会独占锁,阻止其他读写。要深入理解其如何在底层实现读写互斥,需要了解 RWMutex 的内部结构、状态变量、原子操作以及阻塞队列的配合。本文将带你从字段定义方法逻辑、再到典型使用示例,配合ASCII 图解详细说明,帮助你对 Golang 的读写锁实现有一个全方位的认识。


一、读写锁简介

  • sync.Mutex:最基础的互斥锁,只允许一个 goroutine 在临界区执行。所有其他请求锁的 goroutine 都会被阻塞。
  • sync.RWMutex:分为读锁(RLock/RUnlock)和写锁(Lock/Unlock)。

    • 读锁(RLock:允许多个 goroutine 同时持有,只要没有任何持有写锁的 goroutine。
    • 写锁(Lock:独占锁,所有持有读锁或写锁的 goroutine 必须先释放,再由写者获得。

典型用法:

var rw sync.RWMutex
var data = make(map[string]int)

func read(key string) (int, bool) {
    rw.RLock()
    defer rw.RUnlock()
    v, ok := data[key]
    return v, ok
}

func write(key string, value int) {
    rw.Lock()
    defer rw.Unlock()
    data[key] = value
}

在以上示例里,多个 read 可以并发执行,但 write 会阻塞所有当前的读者和写者,直到其完成并释放。


二、RWMutex 的内部结构

在 Go 的源码中(src/sync/rwmutex.go),RWMutex 的定义(简化版)如下:

type RWMutex struct {
    w           Mutex   // 用于用 write-lock 保护的内置互斥锁
    writerCount int32   // 正在等待写锁或者持有写锁的写者数量
    readerCount int32   // 当前持有读锁的读者数量
    readerWait  int32   // 已经判断为需要阻塞等待写者时,仍持有读锁的读者数量
}
  • w Mutex:内部一个 Mutex,用于序列化写锁获取;写者要先拿到这个 Mutex,再等待读者释放后才能进入临界区。
  • writerCount int32:写者计数,既统计当前持有写锁的写者(理论上只能是 1),也统计正在等待写锁的写者数量。每当调用 Lock() 时,就会 atomic.AddInt32(&writerCount, 1)
  • readerCount int32:读者计数,记录当前已经成功获得读锁且未释放的读者数量。对每个调用 RLock() 的 goroutine,会 atomic.AddInt32(&readerCount, 1)RUnlock() 时会 atomic.AddInt32(&readerCount, -1)
  • readerWait int32:读者等待计数,仅在有写者在等待或持有写锁时,额外跟踪那些本来应该释放读锁却暂时继续持有的读者数量,写者会等待读者全部释放后才开始。

下面给出这 4 个字段的视觉示意(ASCII):

┌────────────────────────────────────────────────────────────────┐
│                           RWMutex                              │
│  ┌────────────────────────┬───────────────────────────────────┐  │
│  │        w Mutex         │ Writer Count ( writerCount )      │  │
│  │  (内部互斥锁,用于序列化写者) │   int32                          │  │
│  └────────────────────────┴───────────────────────────────────┘  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ Reader Count ( readerCount )  int32   (当前持有读锁的读者)  │ │
│  └─────────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │ Reader Wait  ( readerWait )    int32   (在写者等待期间仍持有的读者数量) │ │
│  └─────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

三、RLock / RUnlock 的实现逻辑

下面详细剖析 RLockRUnlock 两个方法的核心流程。

3.1 RLock 源码(简化版)

func (rw *RWMutex) RLock() {
    // 1. 试图增加 readerCount
    r := atomic.AddInt32(&rw.readerCount, 1)
    // 2. 如果有写者正在等待或持有写锁,必须阻塞
    if atomic.LoadInt32(&rw.writerCount) != 0 {
        // 标记本来持有读锁的读者数量
        atomic.AddInt32(&rw.readerWait, 1)
        // 等待写者释放(写者释放时会通知所有阻塞的读者)
        rw.w.Lock()
        rw.w.Unlock()
        atomic.AddInt32(&rw.readerWait, -1)
    }
    // 读锁成功获得
}

3.1.1 详细步骤与说明

  1. 增加 readerCount

    r := atomic.AddInt32(&rw.readerCount, 1)
    • 使用原子操作将 readerCount 加 1,表示这个 goroutine 已经“尝试”持有读锁。
    • 返回值 r 记录当前增加后的读者数(可用于后续调试)。
  2. 检测写者存在情况

    if atomic.LoadInt32(&rw.writerCount) != 0 { … }
    • 只要 writerCount 不为 0,说明有写者正等待或持有写锁,则此时需要将本 goroutine 挂入一个等待队列,不可立即返回持有读锁。
    • 在检测到 writerCount>0 的情况下,会执行下面几个原子和阻塞操作:

      1. atomic.AddInt32(&rw.readerWait, 1):将 readerWait(那些在写者等待期间仍持有锁的读者数量)加 1。
      2. 阻塞等待:通过 rw.w.Lock() 阻塞,直到写者最终调用 rw.w.Unlock() 并唤醒此处阻塞。
      3. atomic.AddInt32(&rw.readerWait, -1):解阻塞后,表示当前读者不再持锁,减少 readerWait
  3. 返回

    • 如果 writerCount==0(目前没有写者),直接获得读锁,无需阻塞。
注意:把 readerCount 提前加 1,是为了确保“正在读取”的状态在写者判断时被看到,从而写者会等待读者全部退出(包括本次增加的读者)。如果不先加 readerCount,就会引起竞态:写者误判“无读者”,直接拿到写锁,导致读者跑到写锁内部,破坏互斥。

3.2 RUnlock 源码(简化版)

func (rw *RWMutex) RUnlock() {
    // 1. 减少 readerCount
    newReaderCount := atomic.AddInt32(&rw.readerCount, -1)
    if newReaderCount < 0 {
        panic("RUnlock of unlocked RWMutex")
    }
    // 2. 如果减少后上一步写者正在等待,并且已没有持有读锁的读者了,则通知写者
    if atomic.LoadInt32(&rw.writerCount) != 0 && atomic.LoadInt32(&rw.readerCount) == atomic.LoadInt32(&rw.readerWait) {
        // 唤醒所有正通过 rw.w.Lock() 阻塞的读者,此处用 Broadcast 语义
        rw.w.Unlock()
    }
}

3.2.1 详细步骤与说明

  1. 减少 readerCount

    newReaderCount := atomic.AddInt32(&rw.readerCount, -1)
    • 表示本 goroutine 放弃读锁,将 readerCount 减 1。
    • 如果结果为负,说明调用了过多的 RUnlock(),会抛出 panic。
  2. 判断写者等待并通知

    if atomic.LoadInt32(&rw.writerCount) != 0 && atomic.LoadInt32(&rw.readerCount) == atomic.LoadInt32(&rw.readerWait) {
        rw.w.Unlock()
    }
    • 只有在以下两个条件同时满足时,才“通知等待的写者”:

      1. writerCount != 0:表示至少有一个写者正在等待(或持有锁)。
      2. readerCount == readerWait

        • 此时 readerWait 表示那些在写者等待阶段被“挤”出来、但仍标记“持有读锁”的读者数量。
        • readerCount 表示当前真正持有读锁的读者总数。
        • 当它们相等时,意味着所有“逃逸”到 readerWait 的读者实际上已经释放,此刻写者可以安全获得写锁。
    • 一旦条件满足,就执行 rw.w.Unlock(),相当于唤醒一个或多个在 RLock 中因写者等待而阻塞的读者或写者。通常 rw.w 上有一个等待读锁/写锁的队列,Unlock() 会唤醒队列中的所有阻塞方。
注意:在 RLock 阻塞时,是通过 rw.w.Lock() 将自己放到 w(内部的 Mutex)的等待队列;对应的 RUnlock 只需要调用 rw.w.Unlock(),就会同时唤醒所有在 w 上阻塞的 goroutine(读者或写者),再由它们自行检查能否完成拿锁。

四、Lock / Unlock 的实现逻辑

接下来,剖析写锁(Lock/Unlock)的内部流程。

4.1 Lock 源码(简化版)

func (rw *RWMutex) Lock() {
    // 1. 标记一个写者开始等待
    atomic.AddInt32(&rw.writerCount, 1)
    // 2. 获取内部互斥锁,序列化所有写者
    rw.w.Lock()
    // 3. 等待读者全部释放
    if atomic.AddInt32(&rw.readerCount, -atomic.LoadInt32(&rw.readerCount)) != 0 {
        // 有读者在持锁,放弃 临时计数,写者挂起
        rw.w.Unlock()  // 释放一次,以便其他读者/写者判断
        // 再次尝试:循环等待,直到所有读者都释放
        for {
            if atomic.LoadInt32(&rw.readerCount) == 0 {
                break
            }
            // 阻塞等待,仍通过 rw.w.Lock() 或其他机制
            runtime.Gosched() // 或者再锁再解锁以等待
        }
    }
    // 此时写者拥有 w 锁,且 readerCount 已为 0,可以安全执行写入
}

:上面代码是极度简化的伪代码,用于演示思路,实际源代码更复杂)

4.1.1 详细步骤与说明

  1. 增加写者计数

    atomic.AddInt32(&rw.writerCount, 1)
    • 表明当前有一个写者开始等待或正持有写锁。这个增量保证后续读者在检查 writerCount 时会被发现。
  2. 获取内部互斥锁 w

    rw.w.Lock()
    • 因为 w 是一个普通的 Mutex,所有调用 Lock() 的写者会被序列化(即排队)。只有第一个写者获取到 w 后才能进入下一步。其他写者在这一步就会被阻塞。
  3. 等待读者释放

    • 一旦持有了 w,写者必须确保当前没有任何读者持锁,才能进入写临界区。否则会阻塞。
    • 上述简化实现中,先将 readerCount 减到 0,如果结果非零,就表示仍有读者未释放;此时需要让当前写者“放弃”单次锁(rw.w.Unlock()),去等所有读者都释放后再重试。
    • 实际 Go 源码中,并不会像上面那样循环手动减 readerCount,而是通过判断与 readerWait 的关系来准确阻塞:

      • 真实流程:写者直接 rw.w.Lock(),独占内部 Mutex。
      • 然后检查 readerCount > 0 时,会调用系统调用阻塞,让出 P,等待最后一个读者在 RUnlock 中检测到“写者在等待且所有读者已退出”时,执行 rw.w.Unlock(),唤醒写者。
    • 简而言之:写者只要持有内部 wreaderCount > 0,就会被阻塞在 w.Lock() 阶段;直到读者全部释放并在 RUnlock 中调用 w.Unlock() 才会继续
  4. 执行写操作

    • 当写者走到这里时,意味着:

      1. 它已经获得内部互斥锁 w
      2. 此刻没有任何读者持有读锁(readerCount == 0)。
    • 可以安全地进入临界区执行写操作(对共享资源写入)。

4.2 Unlock 源码(简化版)

func (rw *RWMutex) Unlock() {
    // 1. 减少写者计数
    atomic.AddInt32(&rw.writerCount, -1)
    // 2. 释放内部互斥锁
    rw.w.Unlock()
}

4.2.1 详细步骤与说明

  1. 减少写者计数

    atomic.AddInt32(&rw.writerCount, -1)
    • 表示当前写者即将离开临界区,不再等待或持有写锁;允许新读者/写者进入。
  2. 释放 w

    rw.w.Unlock()
    • 内部互斥锁 w 解锁时,会唤醒所有在该互斥锁上阻塞的 goroutine(可能是等待写锁的写者,也可能是某些在 RLock 中因写者等待而阻塞的读者)。
    • 这些被唤醒的读者会重新检查 writerCount,如果此时写者已经全部离开(writerCount == 0),它们就会正常返回并持有读锁。
    • 同样,等待写锁的写者会竞争 w,第一个成功的写者会进而检测读者情况,以此类推。
注意:因为 Unlock()rw.w.Unlock() 会唤醒同在 w 上等待的所有 goroutine,所以写者 Unlock() 后,所有阻塞在 RLock() 的读者会被一次性唤醒。它们都要先做一个判断,看当前是否还存在等待的写者;如果 writerCount 恰好此时为 0,就直接获得读锁;如果有新写者已经开始等待,则被唤醒的读者又会在 RLock() 中再次阻塞。这样保证写者优先公平性

五、读写锁的状态转换图解(ASCII)

下面用 ASCII 图示简化展示多个读者和写者竞争锁时,RWMutexreaderCountreaderWaitwriterCount 的状态演变。假设有 2 个读者(R1, R2)和 1 个写者(W1)按时间顺序发起操作。

初始状态(所有计数为 0):
readerCount = 0, readerWait = 0, writerCount = 0

Step 1: R1.RLock()
-------------------
readerCount += 1 → 1
writerCount == 0 → 无写者等待
R1 获得读锁

状态: readerCount=1, readerWait=0, writerCount=0

Step 2: R2.RLock()
-------------------
readerCount += 1 → 2
writerCount == 0 → 无写者等待
R2 获得读锁

状态: readerCount=2, readerWait=0, writerCount=0

Step 3: W1.Lock()
-------------------
writerCount += 1 → 1
尝试 rw.w.Lock() → 成功(因为当前只有读者,没有写者,但写者直接拿到内部互斥锁)
检测 readerCount > 0 → true (readerCount=2),写者必须阻塞

W1 阻塞在 rw.w.Lock() 阶段
状态(暂): readerCount=2, readerWait=0, writerCount=1

Step 4: R1.RUnlock()
----------------------
readerCount -= 1 → 1
writerCount != 0(写者在等待) && readerCount == readerWait (1 == 0)? → false
   → 写者尚需等待读者
状态: readerCount=1, readerWait=0, writerCount=1

Step 5: R2.RUnlock()
----------------------
readerCount -= 1 → 0
writerCount != 0 && readerCount == readerWait (0 == 0)? → true
   → 写者被唤醒:执行 rw.w.Unlock()

此时写者 W1 从 阻塞中醒来,再次尝试 rw.w.Lock():
  - 因为之前 `rw.w.Unlock()`,写者会进入临界区
  - 再检测 readerCount(此时 = 0) → 写者可安全写入

状态: readerCount=0, readerWait=0, writerCount=1 (W1 持有内部 Mutex)

Step 6: W1.Unlock()
----------------------
writerCount -= 1 → 0
rw.w.Unlock() → 唤醒所有阻塞在 rw.w 上的 goroutine(如果有新的 R 或新的写者)
状态: readerCount=0, readerWait=0, writerCount=0

此时若有其他 R 或 W 正在等待,都可按顺序竞争锁

上面简化演示了“一轮”读写者竞争的典型流程。可以看到:

  • 读者在调用 RLock() 时,会先递增 readerCount,只要 writerCount == 0,就可直接返回;否则会增加 readerWait 并阻塞在 rw.w.Lock()
  • 写者在调用 Lock() 时,先递增 writerCount 并同样尝试锁住内部 w。如果有任意读者持有锁(readerCount > 0),就会被阻塞。只有当最后一个读者在 RUnlock() 中发现写者在等待且自己释放时,才会调用 rw.w.Unlock() 唤醒写者。
  • 写者持锁后,readerCount 必为 0,表示没有任何读者持锁,可安全执行写操作。完成后,通过 writerCount--rw.w.Unlock(),唤醒其他等待者。

六、示例:使用读写锁保护共享资源

下面通过一个具体示例来演示 RWMutex 在并发场景下的典型用法与性能优势。

package main

import (
    "fmt"
    "sync"
    "time"
)

type SafeMap struct {
    mu  sync.RWMutex
    m   map[string]int
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        m: make(map[string]int),
    }
}

// 读操作:使用 RLock/RUnlock 并发读
func (s *SafeMap) Get(key string) (int, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[key]
    return v, ok
}

// 写操作:使用 Lock/Unlock 独占写
func (s *SafeMap) Set(key string, value int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

func main() {
    sm := NewSafeMap()
    var wg sync.WaitGroup

    // 写者:每秒写一次
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 5; i++ {
            sm.Set("count", i)
            fmt.Println("写入 count =", i)
            time.Sleep(200 * time.Millisecond)
        }
    }()

    // 多个读者:每 100ms 读一次
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 10; j++ {
                v, _ := sm.Get("count")
                fmt.Printf("读者 %d 读到 count = %d\n", id, v)
                time.Sleep(100 * time.Millisecond)
            }
        }(i + 1)
    }

    wg.Wait()
    fmt.Println("All done.")
}

运行示例输出(可能类似):

读者 1 读到 count = 0
读者 2 读到 count = 0
读者 3 读到 count = 0
写入 count = 0
读者 1 读到 count = 0
读者 2 读到 count = 0
写入 count = 1
读者 3 读到 count = 1
...
All done.
  • 并发读者:在写者未写入之前,所有读者可以同时 获取 RLock(),并行读取 count
  • 写者:调用 Lock(),此时会等待读者释放(最多 3 个读者),在读者离开后独占写入;
  • 读者阻塞:当写者持有写锁时,后续的 RLock() 会阻塞在内部的 rw.w.Lock(),直到写者释放。

从高并发表现来看,如果读操作远多于写操作,就能显著提高并发吞吐:多个读者同时执行不会互相排斥,只有在写者到来时才会短暂停顿。


七、深入探讨:写者优先与公平性

在 Go 的 RWMutex 实现中,存在写者优先的倾向,这样能避免写者饥饿(Writer Starvation)。简单总结:

  • 当有写者在等待时,后续的读者会在 RLock 中检测到 writerCount > 0,从而阻塞到 rw.w.Lock()。这时,即使 readerCount 降到 0,写者也能立刻获得写锁。
  • 写者获得写锁后,会一直占用,直到释放;读者要等到写者释放并将 writerCount-- 后,才会重新唤醒。
  • 这保证了写者不会被源源不断的读者抢占而长期等待。但也意味着:一旦有写者等待,后续读者就不会再进入,这对读取频繁、写入偶尔发生的场景最为合适;如果写入非常频繁,则会导致大量读者阻塞,浪费性能。

可以通过一个 ASCII 图示来对比“有写者 vs 无写者”时读者的行为:

(1) 无写者在等待时的并发读:

   Time →
   ┌─────────────────────────────────────────────┐
   │  R1.RLock()  R2.RLock()  R3.RLock()         │
   │  R1.Read()   R2.Read()   R3.Read()          │
   │  … all run concurrently …                   │
   └─────────────────────────────────────────────┘

(2) 写者到来后,阻塞后续的读者:

   Time →
   ┌───────────────────────────────────────────────────────────────────┐
   │ R1.RLock()  R2.RLock()  R3.RLock()                                │
   │ R1.Read()   R2.Read()   R3.Read()                                 │
   │                            W1.Lock() (开始等待,writerCount=1)    │
   │    ┌───────────────┐                                              │
   │    │ R4.RLock()    │  → 发现 writerCount>0,阻塞到 rw.w.Lock()    │
   │    └───────────────┘                                              │
   │    ┌───────────────┐                                              │
   │    │ R5.RLock()    │  → 同样发现 writerCount>0,阻塞              │
   │    └───────────────┘                                              │
   │ R1.RUnlock()  R2.RUnlock()  R3.RUnlock()                          │
   │   → 最后一个读者 R3.RUnlock() 时,rCount=0,                                │
   │      满足 (writerCount>0 && rCount == readerWait),                      │
   │      调用 rw.w.Unlock() 唤醒 W1                                        │
   │ W1 获得写锁 → R4、R5 继续阻塞                                      │
   └───────────────────────────────────────────────────────────────────┘
  • 写者优先:只要有写者等待,后续读者都必须先阻塞,不能再星星点点地进入临界区。这样可避免写者可能被“无限”延迟。
  • 公平性:写者与读者各自排队,当写者获锁后,只有写者释放(并将 writerCount--)后,新的读者才能进入;若多个写者排队,则它们按顺序依次获取。

八、小结与学习指南

  1. 核心字段RWMutex 中的 w Mutex(内部互斥锁)、writerCountreaderCountreaderWait 共同协作,实现了读写互斥与写者优先的策略。
  2. RLock/RUnlock

    • RLock() 先原子递增 readerCount,若发现 writerCount>0,就会挂起在 rw.w.Lock(),并递增 readerWait
    • RUnlock() 原子递减 readerCount,若此时 writerCount>0readerCount==readerWait,说明最后一个读者离开,调用 rw.w.Unlock(),唤醒写者。
  3. Lock/Unlock

    • Lock() 原子递增 writerCount 并获取内部 w,在 readerCount>0 时挂起,直到被最后一个读者唤醒;
    • Unlock() 原子递减 writerCount,并调用 rw.w.Unlock() 唤醒所有等待的读者/写者。
  4. 写者优先:在有写者等待时,后续读者会被阻塞,以保证写者不会被华丽的读者“插队”而长时间饥饿。
  5. 使用场景:当读操作远多于写操作时,RWMutex 可以大幅提升并发性能;如果写操作频繁,则应慎重,因为频繁阻塞读者会带来额外开销。

通过本文中的代码示例ASCII 图解详细步骤说明,希望你能从底层实现层面彻底掌握 Golang sync.RWMutex 的工作原理。在设计并发程序时,依据实际读写比例选择合适的锁策略,既保证线程安全,又能发挥并发性能。

最后修改于:2025年06月05日 10:52

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日