golang源码分析-RWMutex

本文代码基于go 1.22, 其他版本大差不差

前言

上篇博客分析了sync.Mutex的源码,我们在碰到共享资源问题的时候可以使用sync.Mutex来解决。

但是在实际的开发中,我们会遇到一种情况,就是读多写少的场景。大部分请求都是读请求,并不涉及到数据的修改,这个时候如果还使用sync.Mutex的话,会导致大量的读请求被阻塞(没有意义)。这类情况或者说问题,被称为readers-writers问题。

此时就需要本文的主角,也就是sync.RWMutex来解决这个问题。

RWMutex

RWMutex也称为读写锁。它的特点是:

可以有多个协程同时读取共享资源,当有读操作进行时,后续的写操作阻塞,等待当前操作完成。

同一时刻只能有一个协程写共享资源,写操作进行时,后续进来的读操作阻塞。

想像一下一个有“画面感”的场景:

你和朋友一起去厕所,但是你们并不着急,只是想看看坑位构造(两个读协程)。

厕所只有一个坑位(共享资源)。此时坑位没人占用,于是你们两个可以同时进去看看。但是如果你们看之前坑位有人在用(写协程),那么你们就得等待,等ta上完厕所才可以看(读协程被阻塞),毕竟不等直接看的话不太好。

上面这个例子可以知道读写锁的特点,接下看看sync.RWMutex的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type RWMutex struct {
	w           Mutex        // 解决多个写协程竞争问题
	writerSem   uint32       // 写协程等待信号量
	readerSem   uint32       // 读协程等待信号量
	readerCount atomic.Int32 // 读协程数量
	readerWait  atomic.Int32 // 写协程阻塞时,需要等待的读协程完成操作的数量
}

// 支持最大读协程数量
const rwmutexMaxReaders = 1 << 30

字段的含义都写在代码里了,接下来看它提供的方法:

1
2
3
4
5
func (rw *RWMutex) Lock()            // 写锁
func (rw *RWMutex) RLock()           // 读锁
func (rw *RWMutex) RLocker() Locker  // 返回一个Locker接口, 调用它的方法相当于操作读锁
func (rw *RWMutex) RUnlock()         // 读锁解锁
func (rw *RWMutex) Unlock()          // 写锁解锁

这里省略了TryLockTryRLock方法,它们不是本文重点。

RLock与RUnLock

调用RLock方法可以获取读锁,RUnlock方法用于释放读锁。如何使用不是本文重点,接下来直接看实现(简化版):

1
2
3
4
5
6
func (rw *RWMutex) RLock() {
	if rw.readerCount.Add(1) < 0 {
		// A writer is pending, wait for it.
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
	}
}
  1. readerCount加1,很好理解,因为读协程进来了。
  2. 判断加一后的值是否小于0。这有疑问,大家先记着答案:如果小于0,说明此时有写协程在等待。有写协程在等待,那么将当前的读协程阻塞(通过调用runtime_SemacquireRWMutexR(&rw.readerSem, false, 0))。

RUnlock方法的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (rw *RWMutex) RUnlock() {
	if r := rw.readerCount.Add(-1); r < 0 {
		// Outlined slow-path to allow the fast-path to be inlined
		rw.rUnlockSlow(r)
	}
}

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
	if rw.readerWait.Add(-1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}
  1. readerCount减1,很好理解,因为读协程走了。
  2. 判断减1后的值是否小于0,如果小于0,说明有写协程在等待。此时通过内联的方式调用rUnlockSlow方法。
  3. rUnlockSlow方法中,先判断是否解锁一个未加锁的读协程。通过判断原本的readerCount值是否为0,和是否为-rwmutexMaxReaders。前者好理解,后者后续会说明。如果原本未加锁,那么调用fatal方法报错。
  4. 如果readerWait值为0,说明此时没有读协程在操作了,唤醒写协程。

通过阅读RLockRUnlock实现。我们可以知道readerCount不仅像字面意思一样记录读协程的数量,还有一个隐藏的作用:判断是否有写协程在操作。它作为一个复合型字段(一个字段表达多种含义),类似于sync.Mutex中的state字段。

Lock与UnLock

Lock方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (rw *RWMutex) Lock() {
	// First, resolve competition with other writers.
	rw.w.Lock()
	// Announce to readers there is a pending writer.
	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
	// Wait for active readers.
	if r != 0 && rw.readerWait.Add(r) != 0 {
		runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
	}
}
  1. 先通过RWMutex内嵌的Mutex字段w获取写锁,目的是确保只有一个写协程在操作。
  2. 能走到这里,说明写协程获取到锁,正在操作了。此时它先将readerCount减去rwmutexMaxReaders。这样做的目的是:readerCount的值小于0,表示有写协程在操作。这样别的读协程进来时,看到小于0就不会再去获取读锁了。
  3. 变量r代表了当前读协程的数量。如果r不为0,说明有读协程在操作,此时将readerWait加上r,同时阻塞当前写协程。这样做的目的是:让写协程等待所有读协程操作完成,它再去做写操作

UnLock方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (rw *RWMutex) Unlock() {
	// Announce to readers there is no active writer.
	r := rw.readerCount.Add(rwmutexMaxReaders)
	if r >= rwmutexMaxReaders {
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// Allow other writers to proceed.
	rw.w.Unlock()
}
  1. 刚刚对readerCount减去rwmutexMaxReaders,现在再加回来。目的是表示当前写操作完成了。
  2. 判断readerCount是否大于等于rwmutexMaxReaders,如果大于等于,说明解锁一个未加锁的写协程,报错。
  3. 唤醒当前所有等待的读协程,释放信号量,让它们继续读操作。
  4. 最后释放写锁。

本质上写锁是通过sync.Mutex来控制多个写锁间的并发问题。而通过readerCount是否大于0来控制读协程与写协程的并发问题。

总结

读写锁的实现还是比较简单的(相比于sync.Mutex)。

读操作进来时先判断是否有写操作,有的话就阻塞当前协程。反过来写操作进来时,先获取sync.Mutex锁,再反转readerCount的值,让后续读操作阻塞。再判断当前有读操作的话,阻塞写操作。

虽然经常说golang的并发控制:

不要通过共享内存来通信,而应该通过通信来共享内存。

但平时还是基于锁来操作共享资源的。而使用锁的话,我们要根据实际场景选择合适的锁。sync.RWMutex就是为了解决读多写少(readers-writers)的场景。

updatedupdated2024-06-212024-06-21