这篇文章主要介绍“Golang并发之RWMutex怎么使用”,在日常操作中,相信很多人在Golang并发之RWMutex怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang并发之RWMutex怎么使用”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
读写互斥锁是一种同步原语,它允许多个协程同时访问共享资源,同时确保一次只有一个协程可以修改资源。相较于互斥锁,读写互斥锁在读操作比写操作更频繁的情况下,可以带来更好的性能表现。
在 Go
语言中,RWMutex
是一种读写互斥锁的实现,它提供了一种简单有效的方式来管理对共享资源的并发访问。它提供了两种类型的锁:读锁 和 写锁。
1、读锁(RLock()
、TryRLock()
和 RUnlock()
方法)
RWMutex
的读锁是一种共享锁,当一个协程获取了读锁后,其他协程也可以同时获取读锁,从而允许并发的读操作。
2、写锁(Lock()
、TryLock()
和 Unlock()
方法)
RWMutex
的写锁是一种独占锁,当一个协程获取了写锁后,其他协程无法获取读锁或写锁,直到该协程释放写锁。在写锁未被释放之前,任何想要获取读锁或写锁的 goroutine
都会被阻塞。
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
秒钟。由于使用了读写锁,多个读操作可以同时进行,而写操作则会互斥进行,保证了并发安全。
在 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
并发地执行,而且只有在读操作和写操作同时存在时才会进行互斥锁定,有效地提高了并发性能。
为了正确使用读写锁,必须正确使用锁的方法。对于读操作,必须成对使用 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()
,而写操作则会等待读操作完成后才能被唤醒继续执行,从而导致死锁的状态。
到此,关于“Golang并发之RWMutex怎么使用”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。