前面分析了互斥锁,在针对写少读多的场景,更好的选择是使用读写锁。实现读写锁主要解决下列的问题:
- 写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞
- 写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞
- 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞
- 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁
读写锁数据数据结构
源码中定义了读写锁的数据结构:
type RWMutex struct {
w Mutex // 用于控制多个写锁,获得写锁首先需要获取该锁
writerSem uint32 // 写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
readerSem uint32 // 读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
readerCount int32 // 记录读者的个数
readerWait int32 // 记录写阻塞时的读者个数
}
RWMutex 对外提供4个简单接口:
- RLock 读锁定
- RULock 解除读锁定
- Lock 写锁定
- Unlock 解除写锁定
Lock 实现逻辑
写锁定操作需要做2件事:
- 获取互斥锁
- 阻塞等待所有读操作结束
-
写操作是如何阻止写操作?
读写锁里面包含一个互斥锁w
,写锁定必须先获取该互斥锁,如果互斥锁已经被协程A获取,则协程B只能阻塞等待该互斥锁。 -
读操作如何阻止写操作?
读锁定会先将readerCount
的值加1(见下面),此时写操作到来时发现读者数量不为0,会阻塞等待所有读操作结束。
Unlock 实现逻辑
解除写锁定需要做2件事:
- 唤醒因读锁定而被阻塞的协程
- 解除互斥锁,如果有其他协程写锁定操作,将被唤醒
RLock 实现逻辑
读锁定操作需要做2件事:
- 增加读操作计数
- 阻塞等待写操作计数
- 写操作如何阻止读操作?
readerCount 是一个整型值,用于表示读者数量,在不考虑写操作的情况下,每次读锁定将该值加1,每次解除锁定将该值减1,所以readerCount
的取值为[0, N],N为读者个数,实际上最大可支持 个并发读者
当进行写锁定时,会先将readerCount
减去 ,从而readerCount
变成了负值,此时再有读锁定到来时检测到为负值,便知道有写操作在进行,只好阻塞等待。
RUlock 实现逻辑
解除读锁定需要做2件事:
- 减少读操作计数
- 唤醒等待写操作的协程
为什么写锁定不会被饿死 ?
写操作要等待读操作结束后才可以获取到锁,写操作等待期间可能还有新的读操作持续到来,如果写操作要等所有读操作结束,则很可能会被饿死?那是怎么解决这个问题呢?
写操作到来时,会把readerCount
的值复制到readerWait
中,用于标记排在写操作前面的读者个数。
前面的读操作结束后,除了会递减readerCount
的值,还会递减readerWait
的值,当readerWait
的值变为0时唤醒写操作,写操作结束后再唤醒后面的读操作。
网友评论