如何设计一个读写锁,以sync.RWMutex为例

457 阅读2分钟

读写锁适用于多写少读的场景,同时只允许一个写进程存在,多个读操作可以并发进行,设计一个高并发可行的读写锁是一个值得思考的工程问题

难点

  • 保证多个读操作并发,同时阻塞写操作
  • 保证写操作与写操作之间互斥,同时不能有其他读操作
  • 读操作和写操作可以相互感知【当有读操作时写操作被阻塞,读操作都结束时可以唤醒写操作,反之也成立】

总的来说可以概括为以下表格

互斥
YN
NN

其实要解决的问题就是三个,分别是读读共享、写写互斥、读写互斥。进一步分析,其实就是解决以下问题:

  • 多个读操作时共享锁,
  • 多个写操作时只有一个可以获取锁
  • 读写有一个存在时进入等待队列
  • 满足条件时释放锁
  • 读、写线程可以相互感知对方存在,

下面分析讨论golang官方的sync.RWMutex是如何解决以上问题的。

读读共享

多个读操作获取锁时,读操作计数+1,标记当前的读线程数

atomic.AddInt32(&rw.readerCount, 1) 

读写互斥

已有写操作,读操作获取锁时

if atomic.AddInt32(&rw.readerCount, 1) < 0 {
    //readerCount+1<0说明存在写线程,该进入读信号量的等待队列
    runtime_SemacquireMutex(&rw.readerSem, false, 0)
}

已有读操作,写操作获取锁时

//r表示当前读线程的个数
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
//将readerWait加上当前读进程的个数,表示当前写进程需要等待readerCount个读线程释放锁
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
 //如果有其他读进程或者需要等待读进程释放个数>0(即readerCount>0),则进入写信号量等待队列
   runtime_SemacquireMutex(&rw.writerSem, false, 0)
}

写线程存在的通知

//readerCount-rwmutexMaxReaders,让readerCount为负数,通知读线程此时有写线程
atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
//readerCount+rwmutexMaxReaders,让readerCount为非负数,通知读线程写线程已处理完毕
atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)

写写互斥

//通过互斥锁来实现
rw.w.Lock()

深入sync.RWMutex源码

在解决以上三个问题后再来具体看完整的源码(go1.16)

type RWMutex struct {
   w           Mutex  //用于写写互斥的锁
   writerSem   uint32 //等待读线程完成的信号量
   readerSem   uint32 //等待写线程完成的信号量
   readerCount int32  //现有的读线程数
   readerWait  int32  //写线程需要等待释放的读线程数
}
​
const rwmutexMaxReaders = 1 << 30
//获取读锁
func (rw *RWMutex) RLock() {
   if race.Enabled {
      _ = rw.w.state
      race.Disable()
   }
  //现有的读线程数+1
   if atomic.AddInt32(&rw.readerCount, 1) < 0 {
     //readerCount<0,表示此时写线程已经获取的锁,进入等待队列,等待写线程完成释放信号量
     //readerCount在写线程存在时会减去rwmutexMaxReaders
      runtime_SemacquireMutex(&rw.readerSem, false, 0)
   }
   if race.Enabled {
      race.Enable()
      race.Acquire(unsafe.Pointer(&rw.readerSem))
   }
}
//释放读锁
func (rw *RWMutex) RUnlock() {
   if race.Enabled {
      _ = rw.w.state
      race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
      race.Disable()
   }
  //现有的读线程数-1
   if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
      //readerCount<0,表示已有一个写线程在等待释放读锁
      rw.rUnlockSlow(r)
   }
   if race.Enabled {
      race.Enable()
   }
}
func (rw *RWMutex) rUnlockSlow(r int32) {
   if r+1 == 0 || r+1 == -rwmutexMaxReaders {
      race.Enable()
      throw("sync: RUnlock of unlocked RWMutex")
   }
   //readerWait-1,表示写线程需要等待释放的读锁数-1
   if atomic.AddInt32(&rw.readerWait, -1) == 0 {
      //如果所有读锁都释放了,则释放写信号量,唤醒写线程
      runtime_Semrelease(&rw.writerSem, false, 1)
   }
}
//获取写锁
func (rw *RWMutex) Lock() {
   if race.Enabled {
      _ = rw.w.state
      race.Disable()
   }
   //写写互斥
   rw.w.Lock()
   //将readerCount-rwmutexMaxReaders通知其他读线程,存在一个写线程,r等于当前读线程数
   r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
   //readerWait为该写线程需要等待的读锁释放个数
   if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
     //当前读线程数>0或者需要等待释放的读锁个数>0,进入写信号等待队列
      runtime_SemacquireMutex(&rw.writerSem, false, 0)
   }
   if race.Enabled {
      race.Enable()
      race.Acquire(unsafe.Pointer(&rw.readerSem))
      race.Acquire(unsafe.Pointer(&rw.writerSem))
   }
}
//释放写锁
func (rw *RWMutex) Unlock() {
   if race.Enabled {
      _ = rw.w.state
      race.Release(unsafe.Pointer(&rw.readerSem))
      race.Disable()
   }
   //将readerCount+rwmutexMaxReaders,通知其他读线程,写线程已处理完毕
  //r表示当前读线程的个数
   r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
   if r >= rwmutexMaxReaders {
      race.Enable()
      throw("sync: Unlock of unlocked RWMutex")
   }
   //循环释放r个读信号量,以唤醒所有读线程
   for i := 0; i < int(r); i++ {
      runtime_Semrelease(&rw.readerSem, false, 0)
   }
   //允许其他写线程获取锁
   rw.w.Unlock()
   if race.Enabled {
      race.Enable()
   }
}