美文网首页
Go - sync.RWMutex

Go - sync.RWMutex

作者: kyo1992 | 来源:发表于2021-05-15 10:08 被阅读0次

    设计目的

    大多数读请求之间互不影响,在读多写少的场景下,可以分离读写操作,提高读写并发性能.

    限制

    只能读读并发, 读写, 写写操作不并发

    RWMutex

    RWMutex 在某一时刻只能由任意数量的 reader 持有,或者是只被单个的 writer 持有。

    RWMutex 的方法总共有 5 个。

    1. Lock/Unlock:写操作时调用的方法。如果锁已经被 reader 或者 writer 持有,那么,Lock 方法会一直阻塞,直到能获取到锁;Unlock 则是配对的释放锁的方法。
    2. RLock/RUnlock:读操作时调用的方法。如果锁已经被 writer 持有的话,RLock 方法会一直阻塞,直到能获取到锁,否则就直接返回;而 RUnlock 是 reader 释放锁的方法。
    3. RLocker:这个方法的作用是为读操作返回一个 Locker 接口的对象。它的 Lock 方法会调用 RWMutex 的 RLock 方法,它的 Unlock 方法会调用 RWMutex 的 RUnlock 方法。

    RWMutex 的零值是未加锁的状态,所以,当你使用 RWMutex 的时候,无论是声明变量,还是嵌入到其它 struct 中,都不必显式地初始化。

    使用场景

    如果遇到可以明确区分 reader 和 writer goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex。

    实现原理

    RWMutex 是基于 Mutex 实现的

    readers-writers 问题一般有三类,基于对读和写操作的优先级,读写锁的设计和实现也分成三类。

    • Read-preferring:读优先的设计可以提供很高的并发性,但是,在竞争激烈的情况下可能会导致写饥饿。这是因为,如果有大量的读,这种设计会导致只有所有的读都释放了锁之后,写才可能获取到锁。

    • Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。

    • 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。

    Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。

    RWMutex 包含一个 Mutex,以及四个辅助字段 writerSem、readerSem、readerCount 和 readerWait:

    type RWMutex struct {
      w           Mutex   // 互斥锁解决多个writer的竞争
      writerSem   uint32  // writer信号量
      readerSem   uint32  // reader信号量
      readerCount int32   // reader的数量
      readerWait  int32   // writer等待完成的reader的数量
    }
    
    const rwmutexMaxReaders = 1 << 30
    
    • 字段 w:为 writer 的竞争锁而设计;
    • 字段 readerCount:记录当前 reader 的数量(以及是否有 writer 竞争锁);
    • readerWait:记录 writer 请求锁时需要等待 read 完成的 reader 的数量;
    • writerSem 和 readerSem:都是为了阻塞设计的信号量。
    • 这里的常量 rwmutexMaxReaders,定义了最大的 reader 数量。

    写锁加锁过程

    1. 尝试获取写锁,如果锁被占用,则本goroutine会进入自旋或者休眠.
    2. 判断当前执行读操作协程数量,如果不为0,先设置要等待的读操作数量,然后设置等待写等待读信号量进入休眠状态,等待所有读锁执行结束后释放信号量将当前协程唤醒.

    写锁解锁过程

    1. 通过设置readCount成正数,释放读锁.
    2. for循环释放所有因为获取读锁而陷入等待的Groutine.
    3. 释放写锁.

    读锁加锁过程

    1. 直接对readCount进行+1原子操作,>0则代表没有goroutine获取写锁, 读锁获取成功.
    2. 如果<0则代表有goroutine获取写锁,等待读等待写信号量进入休眠状态,等待写锁执行结束后释放信号量.

    读锁解锁过程

    1. 直接对readCount进行-1原子操作,如果>=0代表释放成功.
    2. 如果<0,代表有写操作在等待读锁释放,将readerWait数量-1,如果结果==0,则触发写等待读信号量唤醒尝试获取写锁的goroutine.

    加锁解锁总结

    获取写锁时会先阻塞写锁的获取,后阻塞读锁的获取,解锁时先释放读锁,唤醒等待的读操作,再释放写锁,这种策略能够保证读操作不会被连续的写操作『饿死』。

    相关文章

      网友评论

          本文标题:Go - sync.RWMutex

          本文链接:https://www.haomeiwen.com/subject/uhtcjltx.html