Golang并发之RWMutex的用法详解
作者:陈明勇 发布时间:2024-05-09 14:52:35
前言
在这篇文章 Go Mutex:保护并发访问共享资源的利器 中,主要介绍了 Go
语言中互斥锁 Mutex
的概念、对应的字段与方法、基本使用和易错场景,最后基于 Mutex
实现一个简单的协程安全的缓存。而本文,我们来看看另一个更高效的 Go
并发原语,RWMutex
。
准备好了吗?喝一杯你最喜欢的饮料,随着文章一起进入 RWMutex
令人兴奋的世界!
说明:本文使用的代码基于的 Go 版本:1.20.1
RWMutex
读写互斥锁是一种同步原语,它允许多个协程同时访问共享资源,同时确保一次只有一个协程可以修改资源。相较于互斥锁,读写互斥锁在读操作比写操作更频繁的情况下,可以带来更好的性能表现。
在 Go
语言中,RWMutex
是一种读写互斥锁的实现,它提供了一种简单有效的方式来管理对共享资源的并发访问。它提供了两种类型的锁:读锁 和 写锁。
1、读锁(RLock()
、TryRLock()
和 RUnlock()
方法)
RWMutex
的读锁是一种共享锁,当一个协程获取了读锁后,其他协程也可以同时获取读锁,从而允许并发的读操作。
2、写锁(Lock()
、TryLock()
和 Unlock()
方法)
RWMutex
的写锁是一种独占锁,当一个协程获取了写锁后,其他协程无法获取读锁或写锁,直到该协程释放写锁。在写锁未被释放之前,任何想要获取读锁或写锁的 goroutine
都会被阻塞。
RWMutex 结构体介绍
type RWMutex struct {
w Mutex
writerSem uint32 // 写操作等待者
readerSem uint32 // 读操作等待者
readerCount atomic.Int32 // 持有读锁的 goroutine 数量
readerWait atomic.Int32 // 请求写锁时,需要等待完成的读锁数量
}
RWMutex
由以下字段组成:
w
: 为互斥锁,用于实现写操作之间的互斥。writerSem
:写操作的信号量。当有goroutine
请求写操作时,如果有其他的goroutine
正在执行读操作,则请求写操作的goroutine
将会被阻塞,直到所有的读操作完成后,通过writerSem
信号量解除阻塞。readerSem
:读操作的信号量。当有goroutine
请求读操作时,如果此时存在写操作,则请求读操作的goroutine
将会被阻塞,直到写操作执行完成后,通过readerSem
信号量解除阻塞并继续执行。readerCount
:读操作的goroutine
数量,当readerCount
为正数时,表示有一个或多个读操作正在执行,如果readerCount
的值为负数,说明有写操作正在等待。readerWait
:写操作的goroutine
等待读操作完成的数量。当一个写操作请求执行时,如果此时有一个或多个读操作正在执行,则会将读操作的数量记录到readerWait
中,并阻塞写操作所在的goroutine
。写操作所在的goroutine
会一直阻塞,直到正在执行的所有读操作完成,此时readerWait
的值将被更新为0
,并且写操作所在的goroutine
将被唤醒。
RWMutex
常用方法:
Lock()
:获取写锁,拥有写操作的权限;如果读操作正在执行,此方法将会阻塞,直到所有的读操作执行结束。Unlock()
:释放写锁,并唤醒其他请求读锁的goroutine
。TryLock()
:尝试获取写锁,如果获取成功,返回true
,否则返回false
,不存在阻塞的情况。RLock()
:获取读锁,读锁是共享锁,可以被多个goroutine
获取,但是如果有写操作正在执行或等待执行时,此方法将会阻塞,直到写操作执行结束。RUnlock()
:释放读锁,如果所有读操作都结束并且有等待执行的写操作,则会唤醒对应的goroutine
。TryRlock()
:尝试获取读锁,如果获取成功,返回true
,否则返回false
,不存在阻塞的情况。
简单读写场景示例
package main
import (
"fmt"
"sync"
"time"
)
type Counter struct {
value int
rwMutex sync.RWMutex
}
func (c *Counter) GetValue() int {
c.rwMutex.RLock()
defer c.rwMutex.RUnlock()
return c.value
}
func (c *Counter) Increment() {
c.rwMutex.Lock()
defer c.rwMutex.Unlock()
c.value++
}
func main() {
counter := Counter{value: 0}
// 读操作
for i := 0; i < 10; i++ {
go func() {
for {
fmt.Println("Value: ", counter.GetValue())
time.Sleep(time.Millisecond)
}
}()
}
// 写操作
for {
counter.Increment()
time.Sleep(time.Second)
}
}
上述代码示例中定义了一个 Counter
结构体,包含一个 value
字段和一个 sync.RWMutex
实例 rwMutex
。该结构体还实现了两个方法:GetValue()
和 Increment()
,分别用于读取 value
字段的值和对 value
字段的值加一。这两个方法在访问 value
字段时,使用了读写锁来保证并发安全。
在 main()
函数中,首先创建了一个 Counter
实例 counter
,然后启动了 10
个协程,每个协程会不断读取 counter
并打印到控制台上。同时,main()
函数也会不断对 counter
的 value
值加 1
,每次加 1
的操作都会休眠 1
秒钟。由于使用了读写锁,多个读操作可以同时进行,而写操作则会互斥进行,保证了并发安全。
基于 RWMutex 实现一个简单的协程安全的缓存
在 Go Mutex:保护并发访问共享资源的利器 文章中,使用了 Mutex
实现了一个简单的线程安全的缓存,但并不是最优的设计,对于缓存场景,读操作比写操作更频繁,因此使用 RWMutex
代替 Mutex
会更好。
import "sync"
type Cache struct {
data map[string]any
rwMutex sync.RWMutex
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]any),
}
}
func (c *Cache) Get(key string) (any, bool) {
c.rwMutex.RLock()
defer c.rwMutex.RUnlock()
value, ok := c.data[key]
return value, ok
}
func (c *Cache) Set(key string, value any) {
c.rwMutex.Lock()
defer c.rwMutex.Unlock()
c.data[key] = value
}
上述代码实现了一个协程安全的缓存,通过使用 RWMutex
的读写锁,保证了 Get()
方法可以被多个 goroutine
并发地执行,而且只有在读操作和写操作同时存在时才会进行互斥锁定,有效地提高了并发性能。
RWMutex 易错场景
没有正确的加锁和解锁
为了正确使用读写锁,必须正确使用锁的方法。对于读操作,必须成对使用 RLock()
和 RUnlock()
方法,否则可能会导致程序 panic
或阻塞。
例如:如果缺少 RLock()
,直接使用 RUnlock()
方法,程序将会 panic
,如果缺少 RUnlock()
方法,将会发生阻塞的形象。
同样,对于写操作,必须成对使用 Lock()
和 Unlock()
方法。
最佳实践是使用 defer
来释放锁:为了保证锁总是被释放,即使在运行时错误或提前返回的情况下,也可以在获得锁后立即使用 defer
关键字来调度相应的解锁方法。
rwMutex.RLock()
defer rwMutex.RUnlock()
// 读操作
rwMutex.Lock()
defer rwMutex.Unlock()
// 写操作
重复加锁
重复加锁操作被称为可重入操作。不同于其他一些编程语言的锁实现(例如 Java
的 ReentrantLock
),Go
的 mutex
并不支持可重入操作。
由于 RWMutex
内部是基于 Mutex
实现的写操作互斥,如果发生了重复加锁操作,就会导致死锁。这个易错场景在上篇文章中也提到了,还给出了代码示例,感兴趣的小伙伴可以去看看。
读操作内嵌写操作
当有协程执行读操作时,请求执行写操作的协程会被阻塞。如果在读操作中嵌入写操作的代码,写操作将调用 Lock()
方法,从而导致读操作和写操作之间形成相互依赖关系。在这种情况下,读操作会等待写操作完成后才能执行 RUnlock()
,而写操作则会等待读操作完成后才能被唤醒继续执行,从而导致死锁的状态。
小结
RWMutex
是 Go
中的一种读写锁实现,它通过读锁允许多个 goroutine
同时执行读操作,当有写操作请求时,必须等待所有读操作执行结束后才能执行写操作。
RWMutex
的设计采用了 Write-preferring
方案,即如果有写操作在等待执行,新来的读操作将会被阻塞,以避免写操作的饥饿问题。
根据 RWMutex
的特性,它适用于 读多写少的高并发场景,可以实现并发安全的读操作,从而减少在锁竞争中的等待时间。
虽然它能够给程序带来了性能的提升,然而,如果使用不当,就可能会导致 panic
或死锁等问题。因此,在使用 RWMutex
时需要特别小心,并避免错误的用法。
来源:https://juejin.cn/post/7218554163051413561
猜你喜欢
- 1.优化应用程序和业务逻辑,这个是最重要的。 2.数据库设计阶段范式和反范式的灵活应用。一般情况下,对于频繁访问但是不频繁修改的数据,内部设
- 1、什么是触发器 触发器对表进行插入、更新、删除的时候会自动执行的特殊存储过程。触发器一般用在check
- 假如某个电脑生产商,它的数据库中保存着整机和配件的产品信息。用来保存整机产品信息的表叫做pc;用来保存配件供货信息的表叫做parts。在pc
- 今天给大家介绍一个可以获取当前系统信息的库——psutil利用psutil库可以获取系统的一些信息,如cpu,内存等使用率,从而可以查看当前
- 不知不觉,玩爬虫玩了一个多月了。我愈发觉得,爬虫其实并不是什么特别高深的技术,它的价值不在于你使用了什么特别牛的框架,用了多么了不起的技术,
- 本文实例为大家分享了python+pygame实现坦克大战的具体代码,供大家参考,具体内容如下一、首先导入pygame库二、源码分享#cod
- 经常看见有人问,MSSQL占用了太多的内存,而且还不断的增长;或者说已经设置了使用内存,可是它没有用到那么多,这是怎么一回事儿呢? 首先,我
- os.path.dirname() 获取父目录os.path.basename() #获取文件名或者文件夹名python2缺省为相对路径导入
- python版本为python3.51.要求1)输入用户名密码2)认证成功后显示欢迎信息3)输错三次后锁定2.需求分析1)用户信息存储在文件
- 集合创建集合有两种方式:第一种:T = {11,111,"11"}print(T)# {'11', 11
- 服务器计算数据有时需要大量的时间,使用程序发送一封邮件是一种免费便捷的通知方式,可以让我们及时收到程序中断或者程序运行完成的信息,而不用一直
- python开发者向普通windows用户分享程序,要给程序加图形化的界面(传送门:这可能是最好玩的python GUI入门实例! http
- 本文实例讲述了Python使用win32com实现的模拟浏览器功能。分享给大家供大家参考,具体如下:# -*- coding:UTF-8 -
- MySQL DATE_FORMAT函数简介要将日期值格式化为特定格式,请使用DATE_FORMAT函数。 DATE_FORMAT函数的语法如
- 摘要RepVgg通过结构重参数化让VGG再次伟大。 所谓“VGG式”指的是:没有任何分支结构。即通常
- 本文实例讲述了JS实现单击输入框弹出选择框效果的方法。分享给大家供大家参考,具体如下:运行效果截图如下:完整实例代码如下:<!DOCT
- 方式一:图片+文字row = 0 # 行号col = 1 # 列号icon = QTableWidgetItem(QIcon(".
- 用python画图很多是根据z=f(x,y)来画图的,本博文将三个对应的坐标点输入画图:散点图:import matplotlib.pypl
- 当在设计中我们讨论到,对于一个功能或元素是否应该添加的时候,秉承“如无所需、勿增实体”的原则,我们通常会放弃只有小众/小部分人群才会使用的功
- 用html的form上传文件时,request.FILES为空,没有收到上传来的文件,但是在request.POST里找到了上传的文件名(只