专栏首页Go语言指北Go高阶12,手摸手带你深入了解 RWMutex 实现原理

Go高阶12,手摸手带你深入了解 RWMutex 实现原理

之前我们介绍了互斥锁 Mutex,今天再来介绍下 RWMutex,即读写锁。读写锁是对 Mutex 的改进,在程序中,如果存在读操作多,写操作少的场景,使用 RWMutex 相比 Mutex 的并发能力会有很大的提升。

关于 RWMutex 的使用可以看下之前的文章介绍:并发控制,同步原语 sync 包,下面我们来看下读写锁的实现。

数据结构

源码包 src/sync/rwmutex.go 中定义了读写锁数据结构:

type RWMutex struct {
 w           Mutex  // held if there are pending writers
 writerSem   uint32 // semaphore for writers to wait for completing readers
 readerSem   uint32 // semaphore for readers to wait for completing writers
 readerCount int32  // number of pending readers
 readerWait  int32  // number of departing readers
}
  • w:用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
  • writerSem:写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
  • readerSem:读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
  • readerCount:记录读者的个数
  • readerWait:记录写阻塞时读者的个数

由此可见,读写锁内部还有一个互斥锁,用来将两个写操作隔离开,其他的几个用于隔离读操作和写操作。

接口定义

RWMutex 提供了 4个接口来:

  • Rlock() : 读锁定
  • RUnlock() : 解除读锁定
  • Lock() : 写锁定,与 Mutex 一致
  • Unlock() : 解除写锁定,与 Mutex 一致

Lock()实现逻辑

写操作所做的两件事:

  1. 获取互斥锁
  2. 阻塞等待所有读操作结束(如果有)

func (rw *RWMutex) Lock() 接口实现流程:

Unlock()实现逻辑

解除写操作所做的两件事:

  1. 唤醒因读锁定而被阻塞的协程(如果有)
  2. 解除互斥锁

func (rw *RWMutex) Unlock() 接口实现流程:

RLock()实现逻辑

读锁定所做的两件事:

  1. 增加读操作计数,即 readerCount++
  2. 阻塞等待写操作结束(如果有)

func (rw *RWMutex) RLock() 接口实现流程

RUnlock()实现逻辑

解除读锁定所做的两件事:

  1. 减少读操作计数,即 readerCount—
  2. 唤醒等待写操作的协程(如果有)

func (rw *RWMutex) RUnlock() 接口实现流程:

注意:只有最后一个解除读锁定的协程才会释放信号量,将协程阻塞等待写操作的协程唤醒。

使用场景

写操作阻止写操作

读写锁中包含一个互斥锁 Mutex,写操作必须先获取到此互斥锁,若此互斥锁已被其他协程获取,当前协程会阻塞等待该互斥锁。

写操作阻止读操作

RWMutex.readerCount 是个整型值,表示读者数量,不考虑写操作的情况下,每次读锁定会将该值+1,每次解除读锁定会将该值-1,所以readerCount 的取值为[0, N],N为读者个数,实际上最大可支持 2^30 个并发读者。

当写锁定进行时,会先将 readerCount 减去2^30,readerCount 就变成了负值,此时再有读锁定到来时检测到 readerCount 为负值,便知道有写操作在进行,只好阻塞等待。而真实的读操作个数并不会丢失,只需要将 readerCount 加上 2^30 即可获得。

所以,写操作将 readerCount 变成负值来阻止读操作的。

读操作阻止写操作

读锁定会先将 RWMutext.readerCount 加1,此时写操作到来时发现读者数量不为0,会阻塞等待所有读操作结束。

所以,读操作通过 readerCount 来将来阻止写操作的。

写锁定为什么不会被饿死

写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。

然而,通过 RWMutex.readerWait 可完美解决这个问题。写操作到来时,会把 RWMutex.readerCount 值拷贝到 RWMutex.readerWait 中,用于标记排在写操作前面的读者个数。前面的读操作结束后,除了会递减 RWMutex.readerCount,还会递减 RWMutex.readerWait 值,当 RWMutex.readerWait 值变为0时唤醒写操作。

所以,如上图,前面的读操作结束后会唤醒写操作,写操作结束后会唤醒后面的读操作。


加我微信,拉你进技术交流群:wucs_dd

本文分享自微信公众号 - 微客鸟窝(gophpython),作者:有码无尘

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go高阶11,手摸手带你深入了解 Mutex 实现原理

    互斥锁是对于并发程序的共享资源进行访问控制的主要手段,之前在介绍并发的时候已经对互斥锁的使用进行过介绍:并发控制,同步原语 sync 包

    微客鸟窝
  • golang 多协程的同步方法总结

    之前用 go 写一个小工具的时候, 用到了多个协程之间的通信, 当时随手查了查, 结果查出来一大坨, 简单记录一下. golang中多个协程之间是如何进行通信及...

    烟草的香味
  • 互斥锁与读写锁:如何使用锁完成Go程同步?

    这张图容易让人产生误解,容易让人误以为goroutine1获取的锁,只有goroutine1能释放,其实不是这样的。“秦失其鹿,天下共逐之”。在这张图中,gor...

    程序员LIYI
  • 模糊虚拟与现实的界限,将数字世界握在手中

    VRPinea
  • Golang语言情怀-第39期 Go 语言设计模式 读写锁

    调试程序发现一个报错:fatal error: concurrent map writes 是因为多个goroutine对同一个map产出了竞争,解...

    李海彬
  • go 并发编程

    Go 语言在 sync 包中提供了用于同步的一些基本原语,包括常见的 sync.Mutex、sync.RWMutex、sync.WaitGroup、sync.O...

    haifeiWu
  • Go 并发编程之 RWMutex

    友情提示:此篇文章大约需要阅读 8分钟42秒,不足之处请多指教,感谢你的阅读。 订阅本站

    Meng小羽
  • 当 Go struct 遇上 Mutex

    struct 是我们写 Go 必然会用到的关键字, 不过当 struct 遇上一些比较特殊类型的时候, 例如: Mutex, 你注意过你的程序是否依然正常吗 ?

    haohongfan
  • 刷脸支付问题多,亚马逊选择刷「手掌」,在无人超市正式商用

    最近,亚马逊正式宣布,他们研发的手掌识别技术「Amazon One」正式投入商用。

    量子位
  • 手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理

    一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用。但是这个应用一时半会又找不到源代码存在何处。但是测试小姐姐的活还是一定要帮...

    andyxh
  • Go语言学习路线 - 6.提效篇:不懈地追求提升研发效率

    在入门篇与基础篇之后,我选择做了这一讲提效篇。而在提效篇的推出之前,我也开启Go语言技巧系列的更新,着重分享一些具体的工程化实例,包括错误处理、Go Modul...

    junedayday
  • Gopher 2019 Go并发编程的分享

    昨天参加了 Gopher China 2019 大会,分享了《Go并发编程实践》的主题,在这一篇博客中总结一下。

    李海彬
  • 给大家介绍一下实现Go并发同步原语的基石

    Go是一门以并发编程见长的语言,它提供了一系列的同步原语方便开发者使用,例如sync包下的Mutex、RWMutex、WaitGroup、Once、Cond,以...

    KevinYan
  • 聊聊阿秀过去三年间做的最正确的一件事 | 快来薅羊毛

    我买的大部分是技术书,也有一些非技术书,比如《明朝那些事儿》、《平凡的世界》之类的。

    拓跋阿秀
  • Go语言基于共享变量的并发

    一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在...

    李海彬
  • Go语言基于共享变量的并发

    一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在...

    李海彬
  • Go语言基于共享变量的并发

    一个特定类型的方法和操作函数是并发安全的,那么所有它的访问方法和操作都是并发安全的。导出包级别的函数一般情况下都是并发安全的,package级的变量没法被限制在...

    李海彬
  • Go 语言并发编程系列(十)—— sync 包系列:互斥锁和读写锁

    我们前面反复强调,在 Go 语言并发编程中,倡导「使用通信共享内存,不要使用共享内存通信」,而这个通信的媒介就是我们前面花大量篇幅介绍的通道(Channel),...

    学院君
  • 腾讯云 CIF 工程效能峰会,10 月 19 - 20 日震撼来袭!

    ? 近年来,依托于云计算的飞速发展,腾讯云云产品更新迅猛,云原生生态构建初具规模,越来越多的企业也开始寻求与深入数字化转型之路。但在摸索与实践中,企业往往会进...

    腾讯云中间件团队

扫码关注云+社区

领取腾讯云代金券