likes
comments
collection
share

图解golang读写锁RWMutex

作者站长头像
站长
· 阅读数 16

大家好,我是Anthony_4926。

欢迎访问飞书版本:cnl25x1hkc.feishu.cn/docx/Z6KtdW…

我们先明确一些概念

RWMutex涉及到两个操作:读操作、写操作。请严格区分操作的概念。

在接下来的代码解析过程中,可以发现只有写操作是真正的会加锁,而读操作是不加锁的。

还需要区分写操作拥有锁,和拥有写权限的区别。再次强调只有写操作会加锁,但是,即便是写操作拥有了锁,还需要根据当前是否有读操作正在进行读,来判断写操作是否具有写权限。

读操作会阻塞写操作,使之无法获得写权限。

可能上边的内容会让你有点懵,没关系,向下看就好。

锁结构

type RWMutex struct {
   w           Mutex  // 互斥锁,控制多个写入操作的并发执行
   writerSem   uint32 // 写入操作的信号量
   readerSem   uint32 // 读操作的信号量
   readerCount int32  // 当前读操作的个数
   readerWait  int32  // 当前写入操作需要等待读操作解锁的个数
}
  • w是用来给给读写锁中的写操作申请锁用的,读操作并不会上锁,只是会阻塞写锁写而已。

  • 接下来是两个信号量。为了便于理解,我们将加锁过程简单化,认为协程加锁不成功就会被阻塞,休眠。

    • writerSem:如果写操作加锁不成功,就会将写操作休眠,加入与writerSem关联的休眠队列,等writerSem改变时,如果满足条件,则唤醒一个休眠队列中的写操作。
    • readerSem:如果读操作加锁不成功,就会将读操作休眠,加入到与readerSem关联的休眠队列,等readerSem改变时,如果满足条件,则唤醒休眠的读操作。
  • 最后两个是读操作计数,只不过记录的内容不一样。

    • readerCount:记录的是当前读操作的个数
    • readerWait:记录的是当前写操作需要等待读操作的个数。如上图,写操作得等待它上边的是哪个读操作结束。需要注意的是,同一时间只能有一个写操作来抢锁。

图解golang读写锁RWMutex

获取写锁-Lock()

func (rw *RWMutex) Lock() {
   if race.Enabled {
      _ = rw.w.state
      race.Disable()
   }
   // 第一个写操作先锁住,此时就会阻塞其他写锁,至于能否获得读的权限,还得看是否有读操作需要等待
   rw.w.Lock()
   // rwmutexMaxReaders = 1 << 30
   // 将readerCount设置为极小的值,是为了标记当前有写操作,已经加写锁成功,等待获得写权限
   // 先对readerCount减是为了是为了标记当前有写操作,已经加写锁成功,等待获得写权限
   // 再加回来,是为了获得readerCount的数量
   r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
   // 如果readerCount不等于0,说明之前有读操作,那就应该将readerWait加上r
   // 表示当前写操作需要休眠,等待readerWait个读操作后被唤醒,获得写权限
   if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
      runtime_SemacquireMutex(&rw.writerSem, false)
   }
   if race.Enabled {
      race.Enable()
      race.Acquire(unsafe.Pointer(&rw.readerSem))
      race.Acquire(unsafe.Pointer(&rw.writerSem))
   }
}

关于竞态检测的内容我们可以先不用理会。接下来我们捋一捋Lock()流程。

假设,当前各种操作的状态是下图左边这个样子滴,然后有个协程申请Lock(),于是变成右边

图解golang读写锁RWMutex

第一个写操作先获得锁,此时就会阻塞其他写锁,至于能否获得写操权限,还得看是否有读操作需要等待。

Lock()将readerCount设置为极小的值,是为了标记当前有写操作,已经拥有锁,等待获得写权限。再加回来,是为了获得readerCount的数量。接下来就是判断能否获得写操作权限的逻辑。

如果readerCount不等于0,说明之前有读操作,那就应该将readerWait加上r 表示当前写操作需要休眠,等待readerWait个读操作后被唤醒,获得写权限

图解golang读写锁RWMutex

获取读锁-RLock()

func (rw *RWMutex) RLock() {
   if race.Enabled {
      _ = rw.w.state
      race.Disable()
   }
   // 如果readerCount+1小于0,表示曾经被置为非常小
   // 即,当前有写操作获取了锁。所以,当前读操作需要阻塞休眠,与readerSem进行关联
   if atomic.AddInt32(&rw.readerCount, 1) < 0 {
      runtime_SemacquireMutex(&rw.readerSem, false)
   }
   if race.Enabled {
      race.Enable()
      race.Acquire(unsafe.Pointer(&rw.readerSem))
   }
}

获取写锁时说过,写操作获得锁后,会将readerCount置为一个非常小的数,因此这里如果readerCount+1小于0,则表示,有写操作已经获得锁,当前读操作需要阻塞休眠,并将该协程与readerSem进行关联。

当前状态可以是下图这样

图解golang读写锁RWMutex

释放读锁-RUnlock()

func (rw *RWMutex) RUnlock() {
   if race.Enabled {
      _ = rw.w.state
      race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
      race.Disable()
   }
   // 读操作加锁时,是给readerCount加1,释放锁时,是给readerCount减1
   // 减完后,仍然小于0, 表示当前是有写操作获取了锁滴,而且正在阻塞。
   if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
      // 异常情况判断,不影响整体流程。略过
      if r+1 == 0 || r+1 == -rwmutexMaxReaders {
         race.Enable()
         throw("sync: RUnlock of unlocked RWMutex")
      }
      // 如果给readerCount减1后,仍然小于0,说明有写操作在阻塞
      // 应该给readerWait再减1,等readerWait为0时,说明,可以唤醒写操作了
      if atomic.AddInt32(&rw.readerWait, -1) == 0 {
         runtime_Semrelease(&rw.writerSem, false)
      }
   }
   if race.Enabled {
      race.Enable()
   }
}

读操作加锁时,是给readerCount加1,释放锁时,是给readerCount减1。

减完后,仍然小于0, 表示当前是有写操作获取了锁滴,而且正在阻塞。

有写操作阻塞的话,应该给readerWait再减1,等readerWait为0时,说明,可以唤醒写操作了。

如下图,写操作在获取锁时,readerWait的数量是3,等这三个读操作都结束后,写操作就可以执行了。所以,需要唤醒写操作。你可以自己观察一下代码,唤醒和阻塞调用的是不一样的runtime方法。

图解golang读写锁RWMutex

释放写锁-Unlock()

func (rw *RWMutex) Unlock() {
   if race.Enabled {
      _ = rw.w.state
      race.Release(unsafe.Pointer(&rw.readerSem))
      race.Disable()
   }

   // 加写锁时,给readerCount减了一个很大的数,释放时再将这个数加回来
   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)
   }
   // 读操作释放读锁
   rw.w.Unlock()
   if race.Enabled {
      race.Enable()
   }
}

加写锁时,给readerCount减了一个很大的数,释放时再将这个数加回来。

写操作结束后,唤醒读操作。可以看到,这个地方唤醒读操作唤醒了r次。还是用下边这个图解释一下

图解golang读写锁RWMutex