Sirius
Sirius

目录

Go 语言 读写锁

Go 语言中的读写锁(sync.RWMutex)是 sync 包提供的一个核心同步原语,用于解决读多写少场景下的并发性能问题。它允许多个读操作并发执行,但写操作必须独占访问资源。

下面我们深入分析其设计原理和实现机制。


  • 核心思想

    • 多个读者可以同时读:当没有写者时,多个 Goroutine 可以同时持有读锁,安全地读取共享数据。
    • 写者独占访问:当有写者持有写锁时,任何其他读者或写者都不能获取锁,必须等待。
    • 写者优先于新读者:一旦有写者在等待,新来的读者必须排队等待,防止写者被“饿死”(starvation)。
  • 适用场景

    • 配置信息的读取与更新。
    • 缓存系统的读取与刷新。
    • 任何读操作远多于写操作,并且写操作需要完全互斥的数据结构。

RWMutex 的核心是一个 struct,主要包含以下几个字段:

type RWMutex struct {
    w           Mutex  // 互斥锁 (writer mutex),用于保护写操作和 writerSem
    writerSem   uint32 // 写者信号量,用于阻塞/唤醒等待写锁的 Goroutine
    readerSem   uint32 // 读者信号量,用于阻塞/唤醒等待读锁的 Goroutine
    readerCount int32  // 读者计数器
    readerWait  int32  // 等待的读者计数(写者用)
}
  1. readerCount (int32):

    • 这是最核心的计数器。
    • 正值:表示当前有多少个活跃的读者(正在读)。
    • 负值:表示有写者在等待(并且其绝对值等于等待的写者数量 + 当前活跃读者数量)。这是实现“写者优先”的关键技巧。
    • readerCount == 0 时,表示既无读者也无写者等待。
  2. readerWait (int32):

    • 当写者尝试获取写锁时,它会记录下当时所有未完成的读者数量
    • 写者会等待,直到这些“旧的”读者全部释放读锁(即 readerWait 减到 0),才真正获得写锁。
    • 这确保了写者不会被新来的读者“插队”而无限等待。
  3. writerSemreaderSem (uint32):

    • 基于操作系统信号量(在 Go 中通过 runtime_Semacquire / runtime_Semrelease 实现)。
    • writerSem: 等待写锁的 Goroutine 会在这个信号量上睡眠。
    • readerSem: 在特定情况下(如写者持有锁时新读者到来),新读者会在这个信号量上睡眠。
  4. w Mutex:

    • 一个标准的互斥锁,用于保护 writerSem 的操作以及写锁的获取逻辑,确保写操作的串行化。

func (rw *RWMutex) RLock() {
    // 快速路径:尝试原子地增加 readerCount
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 慢速路径:readerCount 变成了负数,说明有写者在等待
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}
  • 快速路径:使用 atomic.AddInt32 原子地将 readerCount 加 1。
    • 如果结果仍为正数,说明没有写者在等待,该 Goroutine 成功获取读锁,直接返回。
  • 慢速路径:如果 AddInt32 的结果是负数,意味着 readerCount 在加 1 之前已经是负数(即已有写者在等待)。此时,新来的读者不能立即获取锁,必须进入等待队列。它会调用 runtime_SemacquireMutexreaderSem 上睡眠,直到被写者唤醒。
func (rw *RWMutex) RUnlock() {
    // 原子地减少 readerCount
    r := atomic.AddInt32(&rw.readerCount, -1)
    if r < 0 {
        // 说明有写者在等待,我们需要检查是否所有“旧的”读者都已释放
        rw.rUnlockSlow(r)
    }
}

func (rw *RWMutex) rUnlockSlow(r int32) {
    // 将 readerWait 减 1
    if atomic.AddInt32(&rw.readerWait, -1) == 0 {
        // 当最后一个“旧的”读者释放时,唤醒等待的写者
        runtime_Semrelease(&rw.writerSem, false, 1)
    }
}
  • 原子地将 readerCount 减 1。
  • 如果减完后 readerCount 仍然 >= 0,说明没有写者在等待,或者还有其他读者,直接返回。
  • 如果 readerCount < 0,说明有写者在等待。此时调用 rUnlockSlow
    • 它会将 readerWait 减 1。
    • readerWait 减到 0 时,意味着所有在写者请求写锁时尚未完成的读者都已释放读锁。此时,调用 runtime_Semrelease 唤醒在 writerSem 上等待的第一个写者。
func (rw *RWMutex) Lock() {
    // Step 1: 抢占 writer 互斥锁,阻止后续的写者
    rw.w.Lock()
    
    // Step 2: 将 readerCount 减去一个很大的数 (const maxInt32)
    // 这会使 readerCount 变成一个很大的负数,标记“有写者在等待”
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    
    // Step 3: 记录当前有多少读者在运行
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        // 如果还有读者,写者需要等待
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}
  • 抢占 w:首先获取内部的 Mutex (w),这确保了同一时间只有一个写者能进入这个逻辑,也阻止了新的写者加入。
  • 标记写者等待:通过 atomic.AddInt32(&rw.readerCount, -maxInt32)readerCount 设置为一个巨大的负数。这有两个作用:
    1. 后续所有新来的 RLock() 调用都会因为 readerCount 为负而进入慢速路径(睡眠)。
    2. 记录下当前活跃的读者数量(r 是加之前的值)。
  • 设置等待计数:将 readerWait 设置为当前活跃的读者数量 r
  • 等待读者退出:如果 r != 0,说明还有读者在读,写者不能立即获取锁。它会在 writerSem 上睡眠,等待所有“旧的”读者通过 RUnlock()readerWait 减到 0 并唤醒它。
func (rw *RWMutex) Unlock() {
    // Step 1: 将 readerCount 加上 maxInt32,恢复其正常值
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    
    // Step 2: 释放内部的 w 锁
    rw.w.Unlock()
    
    // Step 3: 唤醒所有等待的读者
    if r != 0 {
        runtime_Semrelease(&rw.readerSem, false, int32(r))
    }
}
  • 恢复 readerCount:将之前减去的 maxInt32 加回来,使 readerCount 恢复到正常的计数值(通常是 0,如果有新读者进来则为正)。
  • 释放 w:释放内部互斥锁,允许下一个写者进入。
  • 唤醒读者:如果 r != 0,说明在写者持有锁期间,有 r 个读者在 readerSem 上等待。调用 runtime_Semrelease 一次性唤醒所有这些等待的读者。

特性 说明
读并发 多个 RLock() 可以同时成功,提高读性能。
写独占 Lock() 保证写操作完全互斥。
写者优先 一旦有写者等待,新读者必须排队,防止写者饥饿。
公平性 写者按顺序获取锁(通过 w Mutex 保证),读者在写者释放后被批量唤醒。
性能 读操作在无竞争时非常快(仅一次原子操作)。写操作涉及信号量操作,开销较大。

  1. 不可重入RWMutex 不是可重入锁。同一个 Goroutine 不能在持有读锁的情况下再次获取读锁或写锁,否则会导致死锁。
  2. 避免长时间持有读锁:如果读操作耗时很长,会阻塞所有写操作,可能导致写者饥饿。
  3. 正确配对:必须确保每个 RLock() 都有对应的 RUnlock(),每个 Lock() 都有对应的 Unlock(),最好使用 defer

                            +-----------------+
                            |   No Writers     |
                            +--------+--------+
                                     |
                                     | RLock()
                                     v
                     +-------------------------------+
                     |         Multiple Readers      |
                     |       (readerCount > 0)       |
                     +-------------------------------+
                                     |
                                     | Writer arrives -> Lock()
                                     v
               +------------------------------------------+
               | Writer sets readerCount to large negative|
               | and waits for current readers to finish  |
               +------------------------------------------+
                                     |
                                     | All old readers call RUnlock()
                                     v
               +------------------------------------------+
               | Writer is woken up, holds the lock       |
               +------------------------------------------+
                                     |
                                     | Writer calls Unlock()
                                     v
               +------------------------------------------+
               | readerCount restored, waiting readers    |
               | are all woken up                         |
               +------------------------------------------+