Go并发7 同步原语 - RWMutex 读写锁实现原理与实践

247 阅读3分钟
RWMute

锁分离(读写分离) 是提高性能的锁优化方法之一, 像Java中的ReetrantReadWriteLock以及SampedLock,Go语言中同样也提供了相应的同步原语 RWMutex 。

RWMute特点

  1. RWMutex 在某一时刻只能由任意数量的 reader 持有(并行读),或者是只被单个的 writer 持有(串行写)。
  2. RWMutex 的零值是未加锁的状态,所以,当你使用 RWMutex 的时候,无论是声明变量,还是嵌入到其它 struct 中,都不必显式地初始化。

RWMute方法

  1. Lock/Unlock:写操作时调用的方法。如果锁已经被 reader 或者 writer 持有,那么,Lock 方法会一直阻塞,直到能获取到锁;Unlock 则是配对的释放锁的方法。
  2. RLock/RUnlock:读操作时调用的方法。如果锁已经被 writer 持有的话,RLock 方法会一直阻塞,直到能获取到锁,否则就直接返回;而 RUnlock 是 reader 释放锁的方法。
  3. RLocker:这个方法的作用是为读操作返回一个 Locker 接口的对象。它的 Lock 方法会调用 RWMutex 的 RLock 方法,它的 Unlock 方法会调用 RWMutex 的 RUnlock 方法。

适用场景
可以明确区分 reader 和 writer goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求。

使用读写锁实现一个线程安全的计数器
在这个例子中,Incr 方法每秒调用一次,所以writer 竞争锁的频次是比较低的,而 10 goroutine 每毫秒都要执行一次查询,通过读写锁,可以极大提升计数器的性能,因为在读取的时候,可以并发进行。

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var counter Counter
   for i := 0; i < 10; i++ { // 10个reader
      go func() {
         for {
            fmt.Println(counter.Count()) // 计数器读操作
            time.Sleep(time.Millisecond)
         }
      }()
   }
   for { // 一个writer
      counter.Incr() // 计数器写操作
      time.Sleep(time.Second)
   }
}

// Counter 一个使用读写锁实现线程安全的计数器
type Counter struct {
   mu    sync.RWMutex
   count uint64
}

// Incr 使用写锁保证原子性自增
func (c *Counter) Incr() {
   c.mu.Lock()
   c.count++
   c.mu.Unlock()
}

// Count 使用读锁保护读
func (c *Counter) Count() uint64 {
   c.mu.RLock()
   defer c.mu.RUnlock()
   return c.count
}

读写锁的三种设计与实现

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

RWMute的实现原理
RWMutex是基于Mutex同步原语的,其设计是 Write-preferring ,一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。

当一个或者多个 reader 持有锁的时候,竞争锁的 writer 会等待这些 reader 释放完,才可 能持有这把锁。

RWMutex struct 源码
RWMutex 包含一个互斥锁 Mutex,以及四个辅助字段 writerSem、readerSem(读写信号量)、readerCount 和 readerWait:

type RWMutex struct {
 w Mutex // 互斥锁解决多个writer的竞争
 writerSem uint32 // writer阻塞信号量
 readerSem uint32 // reader阻塞信号量
 readerCount int32 // reader的数量(以及是否有 writer 竞争锁)
 readerWait int32 // writer等待完成的reader的数量
}
const rwmutexMaxReaders = 1 << 30 //定义最大Reader数量

读锁核心代码
readCount
Java中的读写锁是通过states的高低十六位来标记是否有线程持有读或者写锁,而RWMutex则是通过readCount来标记是否有G持有或者竞争锁。

readCount的两重含义

  1. 没有 writer 竞争或持有锁时,readerCount 和我们正常理解的 reader 的计数是一样的;
  2. 如果有 writer 竞争锁或者持有锁时,那么,readerCount 不仅仅承担着 reader 的计数功能,还能够标识当前是否有 writer 竞争或持有锁,在这种情况下,请求锁的 reader 的处理进入第 4 行,阻塞等待锁的释放。

读锁加锁解锁过程

  1. 加锁时,正常情况下,readCount只作为计数,每次会进行加一,如果存在writer竞争, readCount为-1时,说明有Writer等待加锁,此时新来的Reader都会被阻塞。

RRock

func (rw *RWMutex) RLock() {
 //readerCount + 1
 if atomic.AddInt32(&rw.readerCount, 1) < 0 {
 // rw.readerCount是负值的时候,意味着此时有writer等待请求锁,新来的Reader会被阻塞,因为writer优先
 runtime_SemacquireMutex(&rw.readerSem, false, 0)
 }
}
  1. 解锁时,readCount会减一,如果readCount为-1,说明有writer竞争锁,这种情况下会去检查是不是所有的读锁都被释放了,如果是,则唤醒请求写锁的writer。

RUnlock

func (rw *RWMutex) RUnlock() {
 //readCount - 1
 if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
 //如果readerCount是负值,就表示当前有 writer 竞争锁,在这种情况下,还会调用 rUnlockSlow 方法,检查是不是 reader 都释放读锁了,如果读锁都释放了,那么可以唤醒请求写锁的 writer 了。
 rw.rUnlockSlow(r) // 有等待的writer
 }
}

func (rw *RWMutex) rUnlockSlow(r int32) {
 if atomic.AddInt32(&rw.readerWait, -1) == 0 {
 // 最后一个reader了,writer终于有机会获得锁了
 runtime_Semrelease(&rw.writerSem, false, 1)
 }

写锁核心代码
在写锁中,一旦一个 writer 获得了内部的互斥锁,就会反转 readerCount 字段,把它从原来的正整数 readerCount(>=0) 修改为负数(readerCount-rwmutexMaxReaders),让这个字段保持两个含义(既保存了 reader 的数量,又表示当前有 writer)。

写锁加锁解锁原理

  1. 加锁时,当一个writer获得了Mutex时,就会反转readCount字段,告知reader有writer持有或者等待锁; 当readCount不为0时,说明此时有reader持有锁,RWM会将readCount赋值给readerWait字段,此时writer进入堵塞;每有一个reader释放锁,readCount减1,当为0时,writer会被唤醒。 Lock
func (rw *RWMutex) Lock() {
 // 首先解决其他writer竞争问题
 rw.w.Lock()
 // 反转readerCount,告诉reader有writer竞争锁
 r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxRead//记录当前活跃的 reader 数量
 // 如果当前有reader持有锁,那么需要等待
 if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
 runtime_SemacquireMutex(&rw.writerSem, false, 0)
 }
}

  1. 解锁时,当一个 writer 释放锁的时候,它会再次反转 readerCount 字段,这里的反转方法就是给它增加 rwmutexMaxReaders 这个常数值;随后会唤醒之后新来的 reader,并且在RWM的Unlock 返回之前,需要把内部的互斥锁释放,释放完毕后,其他的writer 才可以继续竞争这把锁。 UnLock
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()
}