Go 语言 读写锁
系列 - Golang系列
目录
Go 语言中的读写锁(sync.RWMutex)是 sync 包提供的一个核心同步原语,用于解决读多写少场景下的并发性能问题。它允许多个读操作并发执行,但写操作必须独占访问资源。
下面我们深入分析其设计原理和实现机制。
一、基本概念与使用场景
-
核心思想:
- 多个读者可以同时读:当没有写者时,多个 Goroutine 可以同时持有读锁,安全地读取共享数据。
- 写者独占访问:当有写者持有写锁时,任何其他读者或写者都不能获取锁,必须等待。
- 写者优先于新读者:一旦有写者在等待,新来的读者必须排队等待,防止写者被“饿死”(starvation)。
-
适用场景:
- 配置信息的读取与更新。
- 缓存系统的读取与刷新。
- 任何读操作远多于写操作,并且写操作需要完全互斥的数据结构。
二、核心数据结构 (sync/rwmutex.go)
RWMutex 的核心是一个 struct,主要包含以下几个字段:
type RWMutex struct {
w Mutex // 互斥锁 (writer mutex),用于保护写操作和 writerSem
writerSem uint32 // 写者信号量,用于阻塞/唤醒等待写锁的 Goroutine
readerSem uint32 // 读者信号量,用于阻塞/唤醒等待读锁的 Goroutine
readerCount int32 // 读者计数器
readerWait int32 // 等待的读者计数(写者用)
}
关键字段详解:
-
readerCount(int32):- 这是最核心的计数器。
- 正值:表示当前有多少个活跃的读者(正在读)。
- 负值:表示有写者在等待(并且其绝对值等于等待的写者数量 + 当前活跃读者数量)。这是实现“写者优先”的关键技巧。
- 当
readerCount == 0时,表示既无读者也无写者等待。
-
readerWait(int32):- 当写者尝试获取写锁时,它会记录下当时所有未完成的读者数量。
- 写者会等待,直到这些“旧的”读者全部释放读锁(即
readerWait减到 0),才真正获得写锁。 - 这确保了写者不会被新来的读者“插队”而无限等待。
-
writerSem和readerSem(uint32):- 基于操作系统信号量(在 Go 中通过
runtime_Semacquire/runtime_Semrelease实现)。 writerSem: 等待写锁的 Goroutine 会在这个信号量上睡眠。readerSem: 在特定情况下(如写者持有锁时新读者到来),新读者会在这个信号量上睡眠。
- 基于操作系统信号量(在 Go 中通过
-
w Mutex:- 一个标准的互斥锁,用于保护
writerSem的操作以及写锁的获取逻辑,确保写操作的串行化。
- 一个标准的互斥锁,用于保护
三、工作原理解析
1. 获取读锁 (RLock())
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_SemacquireMutex在readerSem上睡眠,直到被写者唤醒。
2. 释放读锁 (RUnlock())
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上等待的第一个写者。
- 它会将
3. 获取写锁 (Lock())
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设置为一个巨大的负数。这有两个作用:- 后续所有新来的
RLock()调用都会因为readerCount为负而进入慢速路径(睡眠)。 - 记录下当前活跃的读者数量(
r是加之前的值)。
- 后续所有新来的
- 设置等待计数:将
readerWait设置为当前活跃的读者数量r。 - 等待读者退出:如果
r != 0,说明还有读者在读,写者不能立即获取锁。它会在writerSem上睡眠,等待所有“旧的”读者通过RUnlock()将readerWait减到 0 并唤醒它。
4. 释放写锁 (Unlock())
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 保证),读者在写者释放后被批量唤醒。 |
| 性能 | 读操作在无竞争时非常快(仅一次原子操作)。写操作涉及信号量操作,开销较大。 |
五、注意事项
- 不可重入:
RWMutex不是可重入锁。同一个 Goroutine 不能在持有读锁的情况下再次获取读锁或写锁,否则会导致死锁。 - 避免长时间持有读锁:如果读操作耗时很长,会阻塞所有写操作,可能导致写者饥饿。
- 正确配对:必须确保每个
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 |
+------------------------------------------+