Golang sync.RWMutex 深度剖析
之前在学习极客时间Go并发编程实战课时,有人在评论区中贴了几道面试题:
1. 读写锁用过吗,读写锁用在什么样的场景?
2. 说说RWMutex的实现原理?说说RWMutex与Mutex的区别?
3. RWMutex源码看过吗?如果使用Mutex来设计一个RWMutex你有什么思路?
4. 使用读写锁时如何规避死锁问题?
5. 如何监控读写锁的等待情况?你有什么思路?
一、什么是读写锁
二、基本组成与应用
w 是排它锁,如果出现写操作则该锁会处于持有状态
writerSem、readerSem 分别是写操作和读操作的信号量,用于协调多个“写-写”操作和“读-写”操作
readerCount 这个计数器记录当前”读“操作的数量,并作为是否有”写“操作来竞争锁的标识
readerWait 这个计数器记录”写“操作请求锁时,需要等待的”读操作“数量;
Lock/Unlock:”写“操作时调用的方法(也可以简单理解成互斥锁的调用)。如果锁已经被 reader 或者 writer 持有,那么,Lock 方法会一直阻塞,直到能获取到锁
RLock/RUnlock:”读“操作时调用的方法。如果锁已经被 writer 持有的话,RLock 方法会一直阻塞,直到能获取到锁
TryLock/TryRlock:与上述 Lock、RLock 功能一致,但是不会阻塞等待锁的获取,而是直接返回
RLocker:这个方法的作用是为”读“操作返回一个 Locker 接口的对象。它的 Lock 方法会调用 RWMutex 的 RLock 方法,它的 Unlock 方法会调用 RWMutex 的 RUnlock 方法
func main() {var counter Counterfor i := 0; i < 10; i++ { // 10个readergo func() {for {counter.Count() // 计数器读操作time.Sleep(time.Millisecond)}}()}for { // 一个writercounter.Incr() // 计数器写操作time.Sleep(time.Second)}}// 一个线程安全的计数器type Counter struct {mu sync.RWMutexcount uint64}// 使用写锁保护func (c *Counter) Incr() {c.mu.Lock()c.count++c.mu.Unlock()}// 使用读锁保护func (c *Counter) Count() uint64 {c.mu.RLock()defer c.mu.RUnlock()return c.count}
三、读写锁是怎么实现的
读操作:
func (rw *RWMutex) RLock() {if atomic.AddInt32(&rw.readerCount, 1) < 0 {// rw.readerCount是负值的时候,意味着此时有writer等待请求锁,因为writer优先级高,所以把后来的reader阻塞休眠runtime_SemacquireMutex(&rw.readerSem, false, 0)}}func (rw *RWMutex) RUnlock() {if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {rw.rUnlockSlow(r) // 有等待的writer}}func (rw *RWMutex) rUnlockSlow(r int32) {if atomic.AddInt32(&rw.readerWait, -1) == 0 {// 最后一个reader了,writer终于有机会获得锁了runtime_Semrelease(&rw.writerSem, false, 1)}}
对 readerCount 这个计数器的值+1 (表示来了一个读操作)
如果 +1 之后,readerCount 的值是负数,则代表有写操作在等待锁,那么阻塞当前的操作;否则直接退出
对 readerCount 这个计数器的值-1(表示送走了一个读操作)
如果 -1 之后,readerCount 的值是负数,则代表有写操作在等待锁,那么会进入到 rUnlockSlow 这个分支的操作(继续执行);否则直接退出
对 readerWait 这个计数器的值-1 (写操作需要等当前所有的读操作都结束后才会进行写;表示送走了一个读操作之后,写操作需要等待的读操作少了一个)
如果 -1 之后,readerWait 的值是 0,则表示没有进行中的读请求了,可以释放信号量唤起写操作啦!
写操作:
func (rw *RWMutex) Lock() {// 首先解决其他writer竞争问题rw.w.Lock()// 反转readerCount,告诉reader有writer竞争锁r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders// 如果当前有reader持有锁,那么需要等待if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_SemacquireMutex(&rw.writerSem, false, 0)}}func (rw *RWMutex) Unlock() {// 告诉reader没有活跃的writer了r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)// 唤醒阻塞的reader们for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)}// 释放内部的互斥锁rw.w.Unlock()}
调用内部的互斥锁上锁,解决写操作之间竞争的问题
把 readerCount 的值取反,让它小于0;(从而给读操作一个标识,告诉”读“操作有”写“操作要来啦,你们新来的”读“操作统统给我阻塞!!)
把当前 readerCount 的值赋予 readerWait,记一下有多少个读操作需要等待的
如果恰好这个时候 readerWait 为 0,那就没有读操作要等了,直接返回;否则阻塞等待 writerSem 信号量来唤起 (对应上文读操作解锁时的最后一个步骤)
把 readerCount 的值取反,让它恢复大于0的正常状态;这个计数器的值-1(告诉新来的读操作,写操作已经完成啦)
唤醒阻塞的reader们(告诉被阻塞的读操作,写操作已经完成啦)
内部互斥锁释放(这时候其他的写操作又可以正常竞争了)
”写-写“操作:通过内部的互斥锁(sync.Mutex),阻塞写操作的互斥锁上锁
”读-写“操作:readerCount 计数器以及信号量,阻塞写操作的互斥锁上锁成功之后的退出
四、有没有什么坑点
不可重入
不可复制
五、总结
上面便是 sync.RWMutex 的介绍。站在使用者的角度上讲,RWMutex 在某一时刻只能由任意数量的”读”操作持有,或者是只被单个的“写”操作持有,适合读多写少的场景 不知道有没有人产生过这样子的疑问,为什么”写“操作在给”读“操作标识时,要直接取反,而不是加多一个内部成员变量呢?(这里我也不太清楚开发背景,只能分析出是在高并发的情况下可以节约内存空间) 回顾最早提出的5道面试题,行文至此各位同学心中前4道应该都有了答案(我这里就一一不明说了,如果有疑问可以再看一遍第二、三、四个章节的结构体定义和操作流程);至于第 5 道题则需要结合 sync.Mutex 深度解析中的第二篇(可以通过 unsafe 指针读取 sync.Mutex 中的 state 字段,和 sync.RWMutex 中的 readerWait 计数器),这里挖一个坑,看看以后有没有机会详细讲解一下关于 sync.Mutex 和 sync.RWMutex 的监控。
参考资料
Go 1.18 源码 极客时间《Go 并发编程实战课》基本并发原语