找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

914

积分

0

好友

128

主题
发表于 13 小时前 | 查看: 2| 回复: 0

在Go语言的高并发编程中,sync.RWMutex(读写互斥锁)是sync.Mutex的一个重要改进。它在某些特定场景下能提供更精细、更高效的并发控制能力,尤其适用于读取数据频率远远大于写入数据频率的场景。

1. 读写锁要解决的并发问题

读写锁的设计旨在协调对共享资源的读写访问,遵循以下四条核心规则:

  1. 写锁需要阻塞写锁:当一个协程持有写锁时,其他任何尝试获取写锁的协程都必须阻塞等待。
  2. 写锁需要阻塞读锁:当一个协程持有写锁时,其他任何尝试获取读锁的协程也必须阻塞等待。
  3. 读锁需要阻塞写锁:当一个或多个协程持有读锁时,任何尝试获取写锁的协程必须阻塞等待,直到所有读锁被释放。
  4. 读锁不能阻塞读锁:当一个协程持有读锁时,其他协程可以继续获取读锁,实现并行读取。

2. 读写锁的数据结构

让我们先看一下 sync.RWMutex 在Go标准库中的数据结构定义:

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 atomic.Int32 // number of pending readers
    readerWait  atomic.Int32 // number of departing readers
}

各字段的作用如下:

  • w: 一个基础的互斥锁,用于在多个写操作之间实现互斥。要获得写锁,首先必须获取此锁。
  • writerSem: 写操作阻塞等待的信号量。当最后一个持有读锁的协程释放锁时,会释放此信号量来唤醒等待的写协程。
  • readerSem: 读操作阻塞等待的信号量。当持有写锁的协程释放锁时,会释放此信号量来唤醒所有因等待写操作而阻塞的读协程。
  • readerCount: 记录当前正在进行或等待的读者数量
  • readerWait: 记录在写操作被阻塞时,尚未完成的读者数量

从结构可以看出,RWMutex 内部依然包含一个基础的Mutex,用于隔离多个写操作;其他字段则协同工作,用于高效地隔离读操作和写操作。理解这些底层结构是掌握Go并发编程高级特性的关键。

3. 接口定义

RWMutex 提供了以下主要方法:

  • RLock(): 获取读锁
  • RUnlock(): 释放读锁
  • Lock(): 获取写锁,其语义与 Mutex.Lock() 完全一致。
  • Unlock(): 释放写锁,其语义与 Mutex.Unlock() 完全一致。
  • TryLock(): 尝试以非阻塞方式获取写锁(Go 1.18 引入)。
  • TryRLock(): 尝试以非阻塞方式获取读锁(Go 1.18 引入)。

4. Lock() 实现逻辑:获取写锁

写锁(Lock)的获取过程分为两步:

  1. 获取内部的互斥锁 w,以排斥其他写者。
  2. 阻塞等待所有已存在的读操作完成(如果有的话)。

其核心源码如下:

// Lock locks rw for writing.
// If the lock is already locked for reading or writing,
// Lock blocks until the lock is available.
func (rw *RWMutex) Lock() {
    if race.Enabled {
        race.Read(unsafe.Pointer(&rw.w))
        race.Disable()
    }
    // First, resolve competition with other writers.
    rw.w.Lock()
    // Announce to readers there is a pending writer.
    r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
    // Wait for active readers.
    if r != 0 && rw.readerWait.Add(r) != 0 {
        runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
        race.Acquire(unsafe.Pointer(&rw.writerSem))
    }
}

获取写锁的核心在于,它通过将 readerCount 减去一个极大值(rwmutexMaxReaders,即 1 << 30),使其变为负值,从而向后续的读操作宣告“有写者在等待”。如果此时存在活跃读者(r != 0),写者会将自己记录在 readerWait 中,并在 writerSem 信号量上休眠,等待被最后一个读者唤醒。

写锁(Lock)获取流程图
图:写锁获取流程。先竞争互斥锁w,然后判断是否有活跃读者。若有,则等待;若无,则直接获得写锁。

5. Unlock() 实现逻辑:释放写锁

释放写锁(Unlock)的过程是获取的逆过程:

  1. 唤醒所有因为等待本写锁而阻塞的读协程(如果有)。
  2. 释放内部的互斥锁 w,允许其他写者竞争。
func (rw *RWMutex) Unlock() {
    if race.Enabled {
        race.Read(unsafe.Pointer(&rw.w))
        race.Release(unsafe.Pointer(&rw.readerSem))
        race.Disable()
    }

    // Announce to readers there is no active writer.
    r := rw.readerCount.Add(rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        race.Enable()
        fatal("sync: Unlock of unlocked RWMutex")
    }
    // Unblock blocked readers, if any.
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    // Allow other writers to proceed.
    rw.w.Unlock()
    if race.Enabled {
        race.Enable()
    }
}

释放时,首先将 readerCount 恢复为正数(加上 rwmutexMaxReaders),这向新来的读操作宣告“写者已离开”。恢复后的 r 代表了在写锁持有期间累积的、等待的读者数量。写锁通过循环释放 readerSem 信号量,一次性唤醒所有这些等待的读者。最后,释放互斥锁 w

写锁(Unlock)释放流程图
图:写锁释放流程。先恢复readerCount为正,然后唤醒所有等待的读者,最后释放互斥锁。

6. RLock() 实现逻辑:获取读锁

获取读锁(RLock)的逻辑相对直接:

  1. 增加读者计数 readerCount
  2. 如果增加后 readerCount 为负(说明有写者在等待或持有锁),则当前读者在 readerSem 信号量上阻塞,等待写锁释放。
func (rw *RWMutex) RLock() {
    if race.Enabled {
        race.Read(unsafe.Pointer(&rw.w))
        race.Disable()
    }
    if rw.readerCount.Add(1) < 0 {
        // A writer is pending, wait for it.
        runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
    }
    if race.Enabled {
        race.Enable()
        race.Acquire(unsafe.Pointer(&rw.readerSem))
    }
}

读锁(RLock)获取流程图
图:读锁获取流程。增加读者计数,并检查是否有写锁(已持有或正在等待)。若有,则阻塞;若无,则成功获得读锁。

7. RUnlock() 实现逻辑:释放读锁

释放读锁(RUnlock)时:

  1. 减少读者计数 readerCount
  2. 如果当前读者是最后一个离开的读者(在存在写者等待的情况下),则负责唤醒那个等待的写者。
func (rw *RWMutex) RUnlock() {
    if race.Enabled {
        race.Read(unsafe.Pointer(&rw.w))
        race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
        race.Disable()
    }
    if r := rw.readerCount.Add(-1); r < 0 {
        // Outlined slow-path to allow the fast-path to be inlined
        rw.rUnlockSlow(r)
    }
    if race.Enabled {
        race.Enable()
    }
}

慢路径 rUnlockSlow 负责处理有写者等待的情况。关键点在于:即使有写协程在等待,也并非每个读锁释放都会去唤醒它。只有最后一个释放读锁的协程(即 readerWait 递减到0时)才会释放 writerSem 信号量来唤醒等待的写协程。因为写锁必须等待所有读操作都结束后才能安全执行。

读锁(RUnlock)释放流程图
图:读锁释放流程。减少读者计数,并判断当前读者是否为最后一个且存在写等待。若是,则唤醒写者;若否,则直接结束。

8. 核心机制场景分析

理解了上述接口实现后,我们可以更深入地分析其机制。

1. 写操作如何阻止其他写操作?
RWMutex 内嵌了一个 Mutex (w)。任何写操作想要获得锁,都必须先获取这个互斥锁。如果它已被协程A获取(或者A正因等待读者而阻塞),那么协程B在尝试调用 Lock() 时,会在 rw.w.Lock() 这一步被阻塞。因此,写操作通过底层的互斥锁来实现彼此之间的互斥

2. 写操作如何阻止读操作?
readerCount 是一个整数原子变量。没有写操作时,它表示活跃的读者数量。
当写操作调用 Lock() 时,会执行 rw.readerCount.Add(-rwmutexMaxReaders),将 readerCount 减去一个非常大的数(1<<30),使其变为负值。后续的读操作在 RLock() 中执行 rw.readerCount.Add(1) 后,会发现结果仍然小于0,从而知道自己需要等待(有写者存在),于是进入休眠。因此,写操作是通过将 readerCount 置为负值来阻止后续读操作的。真实的读者数量信息并未丢失,可以通过加上 rwmutexMaxReaders 还原。

3. 读操作如何阻止写操作?
读操作在 RLock() 中会将 readerCount 加1。写操作在获取了内部互斥锁 w 后,会检查 readerCount 在调整为负数前所代表的活跃读者数量(即代码中的 r)。如果 r != 0,说明存在活跃读者,写操作就会将自己记录到 readerWait 中并开始等待。因此,读操作是通过维持一个正的 readerCount 来阻止写操作的

4. 为什么写锁定不会被饿死?
这是一个关键设计。如果写操作必须等待所有现有的和未来可能到来的读操作结束,那么在读多写少的场景下,写操作可能会被无限期推迟(饿死)。RWMutex 通过 readerWait 字段完美解决了这个问题。

  • 当写操作到来时,它会将此刻的 readerCount 值(即当前活跃读者数)拷贝到 readerWait 中。这标记了排在写操作前面的读者个数
  • 之后,每个前面持有读锁的协程在释放锁 (RUnlock) 时,除了减少 readerCount,还会减少 readerWait
  • 只有当 readerWait 减少到 0 时(意味着所有在写操作之前就开始的读操作都结束了),最后一个读协程才会唤醒等待的写操作。
  • 在此期间新到来的读操作(在写操作之后调用 RLock)会发现 readerCount 为负,从而被阻塞,必须等待这个写操作完成。这就保证了写操作不会被源源不断的新读请求饿死。

通过对 sync.RWMutex 从接口到源码,再到场景的层层剖析,我们可以清晰地看到它是如何精巧地平衡读写的并发性,并在保证数据一致性的前提下,极大提升读多写少场景的性能。希望这份解析能帮助你在实际的Go并发项目中更自信地运用这把利器。如果你想与其他开发者交流更多关于后端架构高并发的设计经验,欢迎来云栈社区参与讨论。




上一篇:PyDracula GUI框架解析:基于PySide6/PyQt6的现代化桌面应用开发利器
下一篇:破解MySQL半同步复制中大事务的阻塞难题:AliSQL实时传输机制详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 18:16 , Processed in 0.452358 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表