【Go并发编程】RWMutex源码阅读

27 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情

RWMutex

RWMutex就是读写锁,其中读读是不互斥的,写读、读写、写写都是互斥的。根据读和写的优先级分为三种:

  • Read-preferring:读优先,只要有读锁,新来的读可以直接读,写要一直等到所有读完成后才开始,并发性高,可能会导致写饥饿。
  • Write-preferring:写优先,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。
  • 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。

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

源码阅读

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:都是为了阻塞设计的信号量。

读锁

  • RLock:readerCount加一,如果有writer,就阻塞,等待唤醒后获得读锁
  • RUnlock:readerCount减一,如果有writer,唤醒writer
func (rw *RWMutex) RLock() {
   // readerCount加一,rw.readerCount<0,此时有writer等待请求锁
   if atomic.AddInt32(&rw.readerCount, 1) < 0 {
      // 阻塞
      runtime_SemacquireMutex(&rw.readerSem, false, 0)
      // 唤醒后获得读锁
   }
}

func (rw *RWMutex) RUnlock() {
   // readerCount加一减一
   if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
      rw.rUnlockSlow(r) // 唤醒等待的writer
   }
}

写锁

  • Lock:获取w锁,反转readerCount代表有writer,如果有reader就先阻塞,等待唤醒获得写锁
  • Unlock:反转readerCount代表没有writer,唤醒所有的reader
func (rw *RWMutex) Lock() {
   // 保证写的原子性
   rw.w.Lock()
   // 反转readerCount,告诉reader有writer竞争锁
   r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
   // 如果当前有reader持有锁,那么需要等待
   if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
      // 阻塞
      runtime_SemacquireMutex(&rw.writerSem, false, 0)
      // 唤醒后获得写锁
   }
}

func (rw *RWMutex) Unlock() {
   // 告诉reader没有活跃的writer了
   r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)

   // 唤醒阻塞的reader们
   for i := 0; i < int(r); i++ {
      runtime_Semrelease(&rw.readerSem, false, 0)
   }
   // 释放内部的互斥锁
   rw.w.Unlock()
}

Trylock

  • TryRLock:有writer直接返回false,反之readerCount+1,获得读锁
  • TryLock:有writer或者reader直接返回false,否则获得写锁
func (rw *RWMutex) TryRLock() bool {
   for {
      c := atomic.LoadInt32(&rw.readerCount)
      // 有writer,直接false
      if c < 0 {
         return false
      }
      // CAS增加readerCount,获得读锁
      if atomic.CompareAndSwapInt32(&rw.readerCount, c, c+1) {
         return true
      }
   }
}

func (rw *RWMutex) TryLock() bool {
   // 读锁都不能获得,直接false
   if !rw.w.TryLock() {
      return false
   }

   if !atomic.CompareAndSwapInt32(&rw.readerCount, 0, -rwmutexMaxReaders) {
      rw.w.Unlock()
      return false
   }
   return true
}

总结

总体来说,复用mutex保证了写写的互斥,利用readercount记录reader数量和是否有writer,通过这些变量的比较,实现了一个写优先的读写锁,对读多写少的场景下应该有不错的性能。