Go 并发编程实战课
晁岳攀(鸟窝)
前微博技术专家,知名微服务框架 rpcx 作者
25635 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 22 讲
Go 并发编程实战课
15
15
1.0x
00:00/00:00
登录|注册

05| RWMutex:读写锁的实现原理及避坑指南

Kubernetes issue 62464
Docker issue 36840
坑点3:释放未加锁的RWMutex
坑点2:重入导致死锁
坑点1:不可复制
Unlock的实现
Lock的实现
RLock/RUnlock的实现
计数器示例
RWMutex的零值
RWMutex的方法
流行的Go开发项目中的坑
RWMutex的3个踩坑点
RWMutex的实现原理
什么是RWMutex?
思考题
读写锁的实现原理及避坑指南
RWMutex

该思维导图由 AI 生成,仅供参考

你好,我是鸟窝。
在前面的四节课中,我们学习了第一个同步原语,即 Mutex,我们使用它来保证读写共享资源的安全性。不管是读还是写,我们都通过 Mutex 来保证只有一个 goroutine 访问共享资源,这在某些情况下有点“浪费”。比如说,在写少读多的情况下,即使一段时间内没有写操作,大量并发的读访问也不得不在 Mutex 的保护下变成了串行访问,这个时候,使用 Mutex,对性能的影响就比较大。
怎么办呢?你是不是已经有思路了,对,就是区分读写操作。
我来具体解释一下。如果某个读操作的 goroutine 持有了锁,在这种情况下,其它读操作的 goroutine 就不必一直傻傻地等待了,而是可以并发地访问共享变量,这样我们就可以将串行的读变成并行读,提高读操作的性能。当写操作的 goroutine 持有锁的时候,它就是一个排外锁,其它的写操作和读操作的 goroutine,需要阻塞等待持有这个锁的 goroutine 释放锁。
这一类并发读写问题叫作readers-writers 问题,意思就是,同时可能有多个读或者多个写,但是只要有一个线程在执行写操作,其它的线程都不能执行读写操作。
Go 标准库中的 RWMutex(读写锁)就是用来解决这类 readers-writers 问题的。所以,这节课,我们就一起来学习 RWMutex。我会给你介绍读写锁的使用场景、实现原理以及容易掉入的坑,你一定要记住这些陷阱,避免在实际的开发中犯相同的错误。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

RWMutex是Go语言中用于解决读写锁问题的并发原语,通过区分读写操作,提高了并发读取的性能。文章介绍了RWMutex的使用场景、实现原理和避坑指南。RWMutex允许多个goroutine同时持有读锁,但只允许一个goroutine持有写锁,通过Lock/Unlock和RLock/RUnlock方法实现对共享资源的保护。在实际应用中,RWMutex适用于大量并发读、少量并发写的场景,能有效提升性能。文章还解释了RWMutex的实现原理,包括基于Mutex的设计方案和具体字段。总的来说,RWMutex是一个高效的并发原语,适用于读写操作频繁且性能要求较高的场景。 文章详细介绍了RWMutex的实现原理,包括RLock/RUnlock和Lock的具体实现方式。在RLock/RUnlock中,通过对readerCount字段的处理,实现了对reader和writer的优先级控制,保证了并发读取和写入的正确性。而在Lock方法中,通过内部的互斥锁保证了对writer的互斥访问。此外,文章还指出了RWMutex的三个常见踩坑点,如不可复制等,为读者提供了实际应用中需要注意的问题。 总的来说,本文通过深入浅出的方式介绍了RWMutex的概念和实现原理,适合读者快速了解并发编程中读写锁的使用和注意事项。文章还列举了一些流行的Go开发项目中使用RWMutex时出现的问题,以及在实际开发中需要注意的细节。通过这些案例,读者可以更好地理解RWMutex的使用和避坑方法,为并发编程提供了有益的参考。 总体而言,本文对RWMutex进行了全面而深入的介绍,既包括理论知识又有实际案例,适合读者快速了解并发编程中读写锁的使用和注意事项。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 并发编程实战课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(27)

  • 最新
  • 精选
  • DigDeeply
    这门课的质量真高啊,看的酣畅淋漓👍

    作者回复: 这是对作者很高的褒奖,谢谢。此留言应该置顶啊

    2020-12-04
    28
  • 那时刻
    请问老师一个关于select{}问题, func foo() { fmt.Println("in foo") } func goo() { var i int fmt.Println("in goo", i) } func main() { go goo() go foo() select {} } 这个代码会报all goroutines are asleep - deadlock,是不是select{}这种写法不推荐么?

    作者回复: 现在版本的go运行时过于智能了,会把这个场景“误报”成死锁,其实我们的本意是让程序hang在这里。 可以改成sleep,waitgroup或者从命令行读取数据等的方式阻塞主goroutine

    2020-10-23
    8
  • Varphp
    原谅我的小白 请教个问题 RWMutex是读写锁,Mutex就是锁吗?区别在于一个精确到读写 一个只能精确到协程对吗

    作者回复: 第一个问题 对。第二个问题太过笼统所以没法回答,当然文章中已经介绍读写锁的功能,适合写少读多的场景

    2021-01-16
    1
  • David
    func (rw *RWMutex) RLock() { if atomic.AddInt32(&rw.readerCount, 1) < 0 { // rw.readerCount是负值的时候,意味着此时有writer等待请求锁,因为writer优先级高,所以把后来的reader阻塞休眠 runtime_SemacquireMutex(&rw.readerSem, false, 0) } } 这个代码, rw.readerCount 为负数,表示有writer,正在等待,则reader要进行休眠。 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) } } 这里是先获取到了锁,才去修改 rw.readerCount 的值,也就是说 每个writer 只有在获取到锁的情况下,才去把 rw.readerCount改成负数,而读锁是否休眠,也是根据这个值来判断。 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() } 而这个地方 先去释放了 reader,再去释放阻塞的writer 假设一种情况,在lock的时候,先后有a,b两个writer 进来,发现还有其他的reader正在使用,那么a,b进行阻塞,当reader 使用完了后,唤醒了a,a获取到了锁,在a使用期间,也有多个reader 进来,进行的休眠。当a使用完成后,调用unlock方法,先是修改了rw.readerCount(根据rlock的方法,rw.readerCount是唯一判断,是否阻塞的条件,那么,当这个时候,有新的reader进来,就可以无条件使用了);再去唤醒被阻塞的reader(这个情况下,唤醒的reader 也可以进行无条件使用了),最后去释放锁,唤醒了b,b去获取锁的时候,发现有reader在使用,修改rw.readerCount的值(标示有等待的writer),然后进行休眠,当最后一个reader使用完成后,唤醒b,这个时候b才正在获取到了锁。 按照以上逻辑,多个writer的情况下,并没有造成reader出现饥饿状态,反而在释放写锁的那一刹那,新的reader 占了先机,这种情况怎么叫 Write-preferring 方案。我理解的Write-preferring 方案:是只有在没有writer的情况下,才轮到reader执行,多个writer的情况,是一个writer一个writer执行完,才会执行reader。我从代码中看出了不明白的地方,希望老师 帮忙解答一下

    作者回复: 在readers已占有锁的情况下,后续只要有writer存在,会优先writer执行锁,后续的reader需要writer释放锁才可以获取

    2021-01-04
    4
    1
  • 50%
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders 这句老师看我理解的对么。首先前一步反转操作是用原子操作实现的,此时其他的reader可能会改变readerCount字段的状态。虽然看起来加减同一个rwmutexMaxReaders看似结果相等,但在并发的场景下,其他reader读到的readerCount的状态为负值,表示有写锁的情况存在。

    作者回复: 对

    2020-11-07
    1
  • john-jy
    没人发现factorial退出条件是错误的吗?

    作者回复: 👍🏻😁是的, 改成n=1

    2023-06-07归属地:北京
  • Traveller
    第二种死锁的场景有点隐蔽。我们知道,有活跃 reader 的时候,writer 会等待,如果我们在 reader 的读操作时调用 writer 的写操作(它会调用 Lock 方法),那么,这个 reader 和 writer 就会形成互相依赖的死锁状态。Reader 想等待 writer 完成后再释放锁,而 writer 需要这个 reader 释放锁之后,才能不阻塞地继续执行。这是一个读写锁常见的死锁场景。 这是什么意思?完全看不懂

    作者回复: 在同一个goroutine中,先调用读锁,再调用写锁,会导致死锁

    2023-04-27归属地:上海
  • 极客酱酱
    鸟窝老师,麻烦问一下您这边使用的脑图工具是哪个,我正在学习这种用脑图梳理知识点的方法,手头的工具不太友好,手动狗头。

    作者回复: processon

    2022-04-19
  • 8.13.3.27.30
    有一个问题、写错unlock逻辑中、解锁之前会唤醒所有等待读的锁、但是再锁的过程当中并可能会继续来读和写的操作、这些操作这里貌似并不能保证写锁的优先,因为写锁解锁的过程会把后来的读锁也唤醒、而这个时候后来的写锁(但是它先于后来的读锁操作锁)没有办法优先后来的读锁获取到锁,不知道理解是否正确,另外读写锁的实现看上去在极限情况下会导致写饥饿、当然也可能是读饥饿(理论上这种情况不应该使用读写锁)、请教老师我的理解是否正确,如果正确怎么解决写饥饿的问题

    作者回复: 很好的发现,你说的对

    2021-11-27
  • 🐭
    第一个例子中,写操作在主携程里,为什么还要加锁

    作者回复: 嗯,这个例子不太好,应该用两个writer更好理解

    2021-08-06
收起评论
显示
设置
留言
27
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部