golang sync.RWMutex源码解析

58 阅读5分钟

sync.RWMutex

版本: go1.20.3 darwin/arm64

下面有四个场景

  • 写操作如何防止写操作?

直接加互斥锁

  • 写操作是如何阻止读操作的?

RWMutex.readerCount是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1

进行写操作时候,会将RWMutex.readerCount变成负值

  • 读操作是如何阻止写操作的?

每次读的时候,会将RWMutex.readerCount减一,当RWMutex.readerCount变为0后,进行写操作

  • 写操作会不会饿死?

首先解释一下为什么可能有饿死的情况发生:写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。

当写操作来的时候,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,然后每次进行读操作RWMutex.readerCountRWMutex.readerWait都会减1,当readerwait=0时,开始写操作

解析

 type RWMutex struct {
   w           Mutex  // 互斥锁
   writerSem   uint32 // 写操作等待读操作完成的信号量
   readerSem   uint32 // 读操作等待写操作完成的信号量
   readerCount atomic.Int32  // 读锁计数器
   readerWait  atomic.Int32  // 获取写锁时当前需要等待的读锁释放数量
 }
 ​
 // 最大只支持 1 << 30 个读锁
 const rwmutexMaxReaders = 1 << 30

信号量(semaphore)

  • 获取(acquire,又称 wait、decrement 或者 P)
  • 释放(release,又称 signal、increment 或者 V)

获取操作把信号量减一,如果减一的结果是非负数,那么线程可以继续执行。如果结果是负数,那么线程将会被阻塞,除非有其它线程把信号量增加回非负数,该线程才有可能恢复运行)。

释放操作把信号量加一,如果当前有被阻塞的线程,那么它们其中一个会被唤醒,恢复执行。

Go 语言的运行时提供了 runtime_SemacquireMutexruntime_Semrelease 函数,像 sync.RWMutex 这些对象的实现会用到这两个函数。

写锁加锁 Lock()

 func (rw *RWMutex) Lock() {
     // 竞态检测
     if race.Enabled {
         _ = rw.w.state
         race.Disable()
     }
     // 1.使用 Mutex 锁,解决与其他写者的竞争
     rw.w.Lock()
     
     // 2.判断当前是否存在读锁:先通过原子操作改变readerCount(readerCount-rwmutexMaxReaders),
     // 使其变为负数,告诉 RUnLock 当前存在写锁等待;
     // 然后再加回 rwmutexMaxReaders 并赋给r,若r仍然不为0, 代表当前还有读锁
       r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
     
     // 3.如果仍然有其他 Goroutine 持有互斥锁的读锁(r != 0)
     // 会先将 readerCount 的值加到 readerWait中,防止源源不断的读者进来导致写锁饿死,
     // 然后该 Goroutine 会调用 sync.runtime_SemacquireMutex 进入休眠状态,
     // 并等待所有读锁所有者执行结束后释放 writerSem 信号量将当前协程唤醒。
     if r != 0 && rw.readerWait.Add(r) != 0 {
     runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
   }
     // 竞态检测
     if race.Enabled {
         race.Enable()
         race.Acquire(unsafe.Pointer(&rw.readerSem))
         race.Acquire(unsafe.Pointer(&rw.writerSem))
     }
 }

首先会加互斥锁,然后会检查是否有其他的读锁,检查方式就是readerCount原子操作-rwmutexMaxReaders,这个时候readerCount变负的阻止写,然后这个值再补回+rwmutexMaxReaders得到r,如果r!=0,说明此时有其他的读锁,那么为了防止写锁饿死,就将waitCount用原子操作+r,然后调用信号量休眠该goroutine

写锁释放 UnLock()

 func (rw *RWMutex) Unlock() {
   // 竞态检测
   if race.Enabled {
     _ = rw.w.state
     race.Release(unsafe.Pointer(&rw.readerSem))
     race.Disable()
   }
 ​
   // Announce to readers there is no active writer.
   // r现在就是读锁的数量
   r := rw.readerCount.Add(rwmutexMaxReaders)
   // 若超过读锁的最大限制, 触发panic
   if r >= rwmutexMaxReaders {
     race.Enable()
     fatal("sync: Unlock of unlocked RWMutex")
   }
   // Unblock blocked readers, if any.
   // 逐个调用信号量唤醒goroutine
   for i := 0; i < int(r); i++ {
     runtime_Semrelease(&rw.readerSem, false, 0)
   }
   // Allow other writers to proceed.
   // 解锁
   rw.w.Unlock()
   // 竞态检测
   if race.Enabled {
     race.Enable()
   }
 }

没什么好讲的,直接看注释就可以了

就是给readerCount加上max值,然后循环采用信号量的方式唤醒coroutine,最后释放互斥锁

读锁加锁 RLock()

 func (rw *RWMutex) RLock() {
   // 是否开启检测race
   if race.Enabled {
     _ = rw.w.state
     race.Disable()
   }
   //这里分两种情况:
   // 1.此时无写锁 (readerCount + 1) > 0,那么可以上读锁, 并且readerCount原子加1(读锁可重入[只要匹配了释放次数就行])
   // 2.此时有写锁 (readerCount + 1) < 0,所以通过readerSem读信号量, 使读操作睡眠等待
   if rw.readerCount.Add(1) < 0 {
     // A writer is pending, wait for it.
     // 当前有个写锁, 读操作需要阻塞等待写锁释放;
     // 其实做的事情是将 goroutine 排到G队列的后面,挂起 goroutine
     runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
   }
   if race.Enabled {
     race.Enable()
     race.Acquire(unsafe.Pointer(&rw.readerSem))
   }
 }

直接readerCount+1,然后<0说明此时有写锁,调用信号量睡眠

读锁释放 RUnlock()

 func (rw *RWMutex) RUnlock() {
   if race.Enabled {
     _ = rw.w.state
     race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
     race.Disable()
   }
   // 写锁等待状态,检查当前是否可以进行获取;
     // 首先将 readerCount 减1并赋予r,然后分两种情况判断
     //  1.若r大于等于0,读锁直接解锁成功,直接结束本次操作;
     //  2.若r小于0,有一个正在执行的写操作,在这时会调用sync.RWMutex.rUnlockSlow 方法;
   if r := rw.readerCount.Add(-1); r < 0 {
     // Outlined slow-path to allow the fast-path to be inlined
     rw.rUnlockSlow(r)
   }
   if race.Enabled {
     race.Enable()
   }
 }
 ​
 func (rw *RWMutex) rUnlockSlow(r int32) {
       // r + 1 == 0 表示本来就没读锁, 直接执行RUnlock()
     // r + 1 == -rwmutexMaxReaders 表示执行Lock()再执行RUnlock()
   if r+1 == 0 || r+1 == -rwmutexMaxReaders {
     race.Enable()
     fatal("sync: RUnlock of unlocked RWMutex")
   }
   // A writer is pending.
   // 如果当前有写锁等待,则减少一个readerWait的数目
   if rw.readerWait.Add(-1) == 0 {
     // The last reader unblocks the writer.
     runtime_Semrelease(&rw.writerSem, false, 1)
   }
 }

先检查是否直接执行RUnlock()||执行Lock()再执行RUnlock(),然后给waitCount-1,与0比较,如果==0,就调用信号量唤醒goroutine

参考

dongxiem.github.io/2020/06/07/…