读写锁是Mutex的改进版本,在某些场景下可以发挥更加灵活的控制能力,比如读取数据频率远远大于写数据频率的场景。

使用读写锁可以多个读操作同时执行。实现读写锁需要解决如下的问题:

  1. 写锁需要阻塞写锁:一个协程拥有写锁的时候,其他协程写操作需要阻塞。
  2. 写锁需要阻塞读锁:一个协程拥有写锁的时候,其他协程读操作需要阻塞。
  3. 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写操作需要阻塞。
  4. 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程可以进行读操作。

数据结构

读写锁是基于Mutex实现的。在src/sync/rwmtex.go:RWMutex中定义了读写锁数据结构。

type RWMutex struct {
	w Mutex          //用于控制多个写锁
	writeSem uint32  //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
	readerSem uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
	readerCount int32 //记录读者个数
	readerWait int32 //记录写阻塞时读者个数
}

接口

RWMutex提供4个简单的接口来提供服务:

  • RLock:读锁定。
  • RUnlock:解除读锁定。
  • Lock:写锁定,与Mutex完全一致
  • Unlock:解除写锁定,与Mutex完全一致。

Lock实现

  • 获取互斥锁
  • 阻塞等待所有读操作结束

Unlock实现

  • 欢迎因读锁定而被阻塞的线程
  • 解除互斥锁

RLock实现

  • 增加读操作计数,即readerCount++
  • 阻塞等待写操作结束

RUnlock实现

  • 减少读操作计数,即readerCount—。
  • 唤醒等待写操作的协程。只有最后一个解除读锁定的协程才会释放信号量将该写操作的协程唤醒。

场景分析

写操作如何阻止写操作

读写锁包含一个互斥锁,写锁定必须要先获取该互斥锁,如果互斥锁已被协程A获取,意味着协程A获取了互斥锁,那么协程B只能阻塞等待该互斥锁。

所以,写操作以来互斥锁来互斥。

写操作如何阻止读操作

RWMutex中readerCount是个整型值,用于表示读者的数量,每次读锁定将该值加1,每次解除读锁定将该值减1,所以readerCount表示的读者的数量范围为0~N,N实际上为2^30个并发读者。

写锁定的时候,会先将readerCount减去2^30,从而readerCount变成了负值,此时再有读锁定到来时检测到readerCount为负值,便知道当前有写操作再进行,于是阻塞等待。真实的readerCount只要写锁释放的时候再加上2^30即可恢复。

所以,写操作将readerCount变成负值来阻止读操作。

读操作如何阻止写操作

读操作会先将readerCount值加1,写操作到来的时候发现读者数量不为0,会阻塞等待所有读操作结束。

所以,读操作通过readerCount来阻止写操作。

写锁定为什么不会被饿死

写锁定需要阻塞等待读操作结束才能够获取到锁,但是在写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。这个问题就是通过readerWait来解决的。

写操作到来的时候,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数

前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait的值,当RWMutex.readerWait的值为0的时候唤醒写操作。 先执行写操作之前就进行的读操作,然后执行写操作,最后再执行写操作之后的读操作。