Go语言读写锁(RWMutex)底层原理详解
Go语言读写锁(RWMutex)底层原理详解
概述
Go语言的sync.RWMutex
是一种读写锁,允许多个读操作同时进行,但写操作是互斥的。这种锁机制在读多写少的场景下能显著提高并发性能。底层通过互斥锁和原子计数器实现复杂的并发控制。
核心数据结构
type rwmutex struct {
rLock mutex // 保护读操作相关的数据(readers, writer)
readers muintptr // 等待读操作的队列
wLock mutex // 控制写操作的互斥锁
writer muintptr // 等待完成读操作的写操作
readerCount atomic.Int32 // 当前有多少个正在进行的读操作
readerWait atomic.Int32 // 写操作需要等待的读操作的数量
readerPass int32 // 读通过计数
}
字段说明
- rLock: 保护读操作相关的数据(readers, writer队列)
- readers: 等待读操作的队列(muintptr链表结构)
- wLock: 控制写操作的互斥锁,确保写操作互斥
- writer: 等待完成读操作的写操作指针
- readerCount: 当前有多少个正在进行的读操作,关键设计:负数表示有写操作竞争
- readerWait: 写操作需要等待的读操作的数量
- readerPass: 写操作完成后允许通过的读操作数
状态转换机制
1. rlock() 获取读锁流程
rlock()操作流程:
func (rw *rwmutex) rlock() {
// 增加当前读操作的计数,如果 +1 还小于0,说明目前有写锁竞争
if rw.readerCount.Add(1) < 0 {
// 如果有写操作等待,则当前读操作需要挂起,排队等待写操作完成
systemstack(func() {
// 获取rLock锁
lock(&rw.rLock)
if rw.readerPass > 0 {
// 如果写操作已经完成,跳过当前读操作
rw.readerPass -= 1
unlock(&rw.rLock)
} else {
// 将当前读操作加入等待队列,等待写操作释放锁
m := getg().m
m.schedlink = rw.readers
rw.readers.set(m)
unlock(&rw.rLock)
// 当前读操作挂起,等待写操作唤醒
notesleep(&m.park)
noteclear(&m.park)
}
})
}
}
关键点:
- 原子操作递增
readerCount
- 如果结果为负数,说明有写者在等待,当前读者需要阻塞
- 这种设计实现了写者优先的策略
2. runlock() 解锁读锁流程
runlock()操作流程:
func (rw *rwmutex) runlock() {
// 如果r小于0说明此时有writer竞争
if r := rw.readerCount.Add(-1); r < 0 {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
throw("runlock of unlocked rwmutex")
}
// 将等待reader的计数减1,如果值==0,说明读等待都处理完了
// 此时需要唤醒写等待
if rw.readerWait.Add(-1) == 0 {
lock(&rw.rLock)
w := rw.writer.ptr()
if w != nil {
notewakeup(&w.park)
}
unlock(&rw.rLock)
}
}
}
关键点:
- 原子操作递减
readerCount
- 如果递减后为负数,说明有写者在等待
- 当
readerWait
减到0时,唤醒等待的写者
3. lock() 获取写锁流程
lock()操作流程:
const rwmutexMaxReaders = 1 << 30
func (rw *rwmutex) lock() {
// 获取写互斥锁,确保只有一个写操作可以执行
lock(&rw.wLock)
m := getg().m
// 将readerCount - 最大读锁数,肯定得到一个负数
// 不会丢失当前正在进行的读操作数量,又可以将值设置为负数
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
// 获取读锁
lock(&rw.rLock)
// 如果有读操作正在进行,写操作需要等待它们完成
if r != 0 && rw.readerWait.Add(r) != 0 {
// 等待读操作完成后才能执行写操作
systemstack(func() {
// 设置当前写操作为等待状态
rw.writer.set(m)
unlock(&rw.rLock)
// 当前写操作挂起,等待读操作唤醒
notesleep(&m.park)
noteclear(&m.park)
})
} else {
unlock(&rw.rLock)
}
}
关键点:
- 先获取基础互斥锁
w
- 将
readerCount
减去一个很大的数(rwmutexMaxReaders
),使其变为负数 - 等待现有读者完成(通过
readerWait
计数)
4. unlock() 解锁写锁流程
unlock()操作流程:
func (rw *rwmutex) unlock() {
// 将readerCount复原,表示当前写操作已经完成
r := rw.readerCount.Add(rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
// 如果没有锁被持有,抛出异常
throw("unlock of unlocked rwmutex")
}
// 获取rLock锁,操作读者队列
lock(&rw.rLock)
// 遍历并唤醒所有在读者队列中的等待操作
for rw.readers.ptr() != nil {
reader := rw.readers.ptr()
rw.readers = reader.schedlink
reader.schedlink.set(nil)
notewakeup(&reader.park)
r -= 1
}
unlock(&rw.rLock)
// 释放写锁,允许其他写操作进行
unlock(&rw.wLock)
}
关键点:
- 将
readerCount
恢复为正数 - 唤醒所有等待的读者
- 最后释放基础互斥锁
优先级策略
写者优先机制
Go的RWMutex实现了写者优先的策略,这是通过以下机制实现的:
- 负数标记:当写者到来时,将
readerCount
设置为负数 - 新读者阻塞:新来的读者发现
readerCount
为负数时会阻塞 - 写者等待:写者会等待现有读者完成,但不会让新读者"插队"
避免饥饿
这种设计避免了写者饥饿问题:
- 如果不断有新读者到来,写者可能会永远等待
- 通过负数标记,新读者会被阻塞,确保写者最终能获得锁
并发控制流程
读操作并发
graph TD
A[Reader1请求读锁] --> B[readerCount++]
C[Reader2请求读锁] --> D[readerCount++]
B --> E[Reader1获得读锁]
D --> F[Reader2获得读锁]
E --> G[Reader1释放读锁]
F --> H[Reader2释放读锁]
G --> I[readerCount--]
H --> J[readerCount--]
读写互斥
graph TD
A[Reader持有读锁] --> B[Writer请求写锁]
B --> C[readerCount设为负数]
C --> D[Writer等待]
A --> E[Reader释放读锁]
E --> F[readerCount--]
F --> G[检查readerWait]
G --> H[唤醒Writer]
H --> I[Writer获得写锁]
性能特点
优点
- 高并发读:多个读操作可以同时进行
- 写者优先:避免写者饥饿
- 公平性:在读写竞争中保持相对公平
缺点
- 写操作开销:写操作需要等待所有读者完成
- 内存开销:需要维护多个计数器和信号量
- 复杂度:实现逻辑相对复杂
使用场景
适合场景
- 读多写少:如配置文件读取、缓存查询
- 读操作耗时短:快速读取,避免长时间持有锁
- 写操作不频繁:偶尔的更新操作
不适合场景
- 写操作频繁:会导致读者频繁阻塞
- 读操作耗时:长时间持有读锁会影响写操作
- 需要严格公平:某些场景可能需要更复杂的公平策略
总结
Go语言的RWMutex通过巧妙的设计实现了高效的读写锁机制:
- 原子操作:使用原子操作保证计数器的准确性
- 信号量机制:通过信号量实现等待/唤醒机制
- 负数标记:用负数表示写者等待状态
- 写者优先:避免写者饥饿问题
这种设计在读多写少的场景下能显著提高并发性能,是Go并发编程中的重要工具。
注意:在实际使用中,要避免在持有锁时进行耗时操作,以免影响其他goroutine的执行。同时,要确保锁的正确释放,避免死锁。
本文来自博客园,作者:元贞,转载请注明原文链接:https://www.cnblogs.com/yuleicoder/p/19087109