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 -= 1unlock(&rw.rLock)} else {// 将当前读操作加入等待队列,等待写操作释放锁m := getg().mm.schedlink = rw.readersrw.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 << 30func (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.schedlinkreader.schedlink.set(nil)notewakeup(&reader.park)r -= 1}unlock(&rw.rLock)// 释放写锁,允许其他写操作进行unlock(&rw.wLock)
}
关键点:
- 将
readerCount
恢复为正数 - 唤醒所有等待的读者
- 最后释放基础互斥锁
优先级策略
写者优先机制
Go的RWMutex实现了写者优先的策略,这是通过以下机制实现的:
- 负数标记:当写者到来时,将
readerCount
设置为负数 - 新读者阻塞:新来的读者发现
readerCount
为负数时会阻塞 - 写者等待:写者会等待现有读者完成,但不会让新读者"插队"
避免饥饿
这种设计避免了写者饥饿问题:
- 如果不断有新读者到来,写者可能会永远等待
- 通过负数标记,新读者会被阻塞,确保写者最终能获得锁
并发控制流程
读操作并发
graph TDA[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 TDA[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的执行。同时,要确保锁的正确释放,避免死锁。