开发者

Golang并发之RWMutex的用法详解

开发者 https://www.devze.com 2023-04-06 10:47 出处:网络 作者: 陈明勇
目录前言RWMutexRWMutex 结构体介绍简单读写场景示例基于 RWMutex 实现一个简单的协程安全的缓存RWMutex 易错场景没有正确的加锁和解锁重复加锁读操作内嵌写操作小结前言
目录
  • 前言
  • RWMutex
  • RWMutex 结构体介绍
  • 简单读写场景示例
  • 基于 RWMutex 实现一个简单的协程安全的缓存
  • RWMutex 易错场景
    • 没有正确的加锁和解锁
    • 重复加锁
    • 读操作内嵌写操作
  • 小结

    前言

    在这篇文章 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 都会被阻塞。

    Golang并发之RWMutex的用法详解

    RWMutex 结构体介绍

    type RWMutex struct {
       w           Mutex        
       writerSem   uint32       // 写操作等待者
       readerSem   uint32       // 读操作等待者 
       readerCount atomic.Int32 // 持有读锁的 goroutine 数量
       readerWait  atomic.Int32 // 请求写锁时,需要等待完成的读锁数量
    }

    RWMutex 由以下字段组成:

    • w: 为互斥锁,用于实现写操作之间的互斥。
    • writerSem:写操作的信号量。当有 goroutine 请求写操作时,如果有其他的开发者_JAVA开发 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 获取,但是http://www.devze.com如果有写操作正在执行或等待执行时,此方法将会阻塞,直到写操作执行结束。
    • 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() 函数也会不断对 countervalue 值加 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.RUnloXnIxZSLKck()
       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(),而写操作则会等待读操作完成后才能被唤醒继续执行,从而导致死锁的状态。

    小结

    RWMutexGo 中的一种读写锁实现,它通过读锁允许多个 goroutine 同时执行读操作,当有写操作请求时,必须等待所有读操作执行结束后才能执行写操作。

    RWMutex 的设计采用了 Write-preferring 方案,即如果有写操作在等待执行,新来的读操作将会被阻塞,以避免写操作的饥饿问题。

    根据 RWMutex 的特性,它适用于 读多写少的高并发场景,可以实现并发安全的读操作,从而减少在锁竞争中的等待时间。

    虽然它能够给程序带来了性能的提升,然而,如果使用不当,就可能会导致 panic 或死锁等问题。因此,在使用 RWMutex 时需要特别小心,并避免错误的用法。

    以上就是golang并发之RWMutex的用法详编程客栈解的详细内容,更多关于Golang RWMutex的资料请关注我们其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号