Go - sync.RWMutex
设计目的
大多数读请求之间互不影响,在读多写少的场景下,可以分离读写操作,提高读写并发性能.
限制
只能读读并发, 读写, 写写操作不并发
RWMutex
RWMutex 在某一时刻只能由任意数量的 reader 持有,或者是只被单个的 writer 持有。
RWMutex 的方法总共有 5 个。
- Lock/Unlock:写操作时调用的方法。如果锁已经被 reader 或者 writer 持有,那么,Lock 方法会一直阻塞,直到能获取到锁;Unlock 则是配对的释放锁的方法。
- RLock/RUnlock:读操作时调用的方法。如果锁已经被 writer 持有的话,RLock 方法会一直阻塞,直到能获取到锁,否则就直接返回;而 RUnlock 是 reader 释放锁的方法。
- 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 数量。
写锁加锁过程
- 尝试获取写锁,如果锁被占用,则本goroutine会进入自旋或者休眠.
- 判断当前执行读操作协程数量,如果不为0,先设置要等待的读操作数量,然后设置等待写等待读信号量进入休眠状态,等待所有读锁执行结束后释放信号量将当前协程唤醒.
写锁解锁过程
- 通过设置readCount成正数,释放读锁.
- for循环释放所有因为获取读锁而陷入等待的Groutine.
- 释放写锁.
读锁加锁过程
- 直接对readCount进行+1原子操作,>0则代表没有goroutine获取写锁, 读锁获取成功.
- 如果<0则代表有goroutine获取写锁,等待读等待写信号量进入休眠状态,等待写锁执行结束后释放信号量.
读锁解锁过程
- 直接对readCount进行-1原子操作,如果>=0代表释放成功.
- 如果<0,代表有写操作在等待读锁释放,将readerWait数量-1,如果结果==0,则触发写等待读信号量唤醒尝试获取写锁的goroutine.
加锁解锁总结
获取写锁时会先阻塞写锁的获取,后阻塞读锁的获取,解锁时先释放读锁,唤醒等待的读操作,再释放写锁,这种策略能够保证读操作不会被连续的写操作『饿死』。