Go 老是忘记读写锁的实现怎么办
前言
读写锁是常用的几种锁之一,原理也很简单,读和读线程间不互斥,写和写线程间互斥,读和写线程间互斥。
但知道原理的我,每次想到读写锁是怎么实现时,就老是忘记,真让人头大。于是我决定通过源码分析之后,能够自己推导出读写锁的实现逻辑,从此无需反复记忆!
读写锁类型
Go用的是协程,因此以下用协程代替线程描述。
在Go中,实现的是写优先的读写锁,即有写协程进来时,后面进来的读协程都堵塞,不会与写协程去竞争锁,这样就保证了写协程能尽快运行。
读写锁结构
基于写优先的性质,则可以把读协程分为两部分。
- 排在写协程前面的读协程,即写线程要等待读协程。
- 排在写协程后面的读协程,即因为写协程堵塞的读协程。
我们再看读写锁的源码结构。
type RWMutex struct {
w Mutex // 互斥锁
writerSem uint32 // 写协程的信号量
readerSem uint32 // 读协程的信号量
readerCount int32 // 读协程的总数
readerWait int32 // 写协程要等待的读协程数量
}
RWMutex
结构体中,用了readerCount
记录读协程的总数,用readerWait
记录写协程要等待的读协程数量,那么排在写协程后面的读协程数 = readerCount - readerWait。
readerCount
和readerWait
定义尤为重要,之后我们都是通过写优先的性质及这两变量的定义来推导出读写锁的实现。
判断是否有加写锁了
readerCount的初始值为0,此时读协程的总数确实为0,但若有写协程加写锁了,则readerCount = readerCount - rwmutexMaxReaders,rwmutexMaxReaders = 1 << 30,是一个很大的常数,因此readerCount会变为一个很大的负数。所以readerCount < 0 时代表有写协程加写锁了。
加读锁
当有个协程要加读锁的时候,按业务需求我们可以推导出:
- 读协程的总数readerCount加一
- 如果readerCount < 0, 说明前面有写协程加写锁了,故要堵塞当前读协程。
源码如下,用了atomic.AddInt32
方法实现原子操作,对readerCount加一后判断是否小于0,是则使用runtime_SemacquireMutex
休眠当前协程。
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
解读锁
当有个协程要解读锁的时候,请思考下业务是什么个逻辑的?
- 当然要将readerCount减一
- 该读协程之前有读锁,说明是写协程要等待完成的读协程,因此解锁后,readerWait要减一
- 如果readerWait等于0了,即没有上锁的读协程了,则唤醒写协程。
源码如下,都是要atomic.AddInt32进行加减操作,保护readerCount和readerWait变量。在readerWait等于0时,使用runtime_Semrelease
唤醒写协程。
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
加写锁
加写锁比较复杂,当有个协程要加写锁的时候,所需步骤如下:
- 先与其他写协程竞争互斥锁
- 拿到锁后,将readerCount减去rwmutexMaxReadersm,用于堵塞后边的读协程
- 之前的readerCount的值,代表写协程要等待的读协程数量,因此readerWait要加上原始的readerCount的值
- 若原始readerCount不等于0且readerWait也不等于0,说明有读协程加锁了,则写协程要休眠等待。
源码如下,这里先将readerCount减去rwmutexMaxReaders,再用r变量记录原始的readerCount的值。若r != 0且readerWait加上原始的readerCount的值后也不等于0,则休眠该写协程。
func (rw *RWMutex) Lock() {
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
解写锁
当有个协程要解写锁的时候,按业务需求我们可以推导出:
- 将readerCount加上rwmutexMaxReaders,使readerCount复原变为0或正数
- 此时readerWait肯定为0,则readerCount代表了堵塞的协程数,于是循环readerCount次,将后面堵塞的所有读协程都唤醒
- 将写锁解开
源码如下,通过循环,使用runtime_Semrelease唤醒了剩余的读协程。
func (rw *RWMutex) Unlock() {
// Announce to readers there is no active writer.
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
throw("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()
}
信号量作用
读写锁的各个功能都解析完毕,但有读者就疑问了,写信号量和读信号量有什么作用呢?
其实设计了两个信号是为了实现读写协程的同步关系的。
- 只有写协程解锁了,才能让读协程加锁
- 只有读协程解锁了,才能让写协程加锁
具体实现为:
runtime_SemacquireMutex
让对应信号量的协程休眠runtime_Semrelease
让对应信号量的协程唤醒
总结
Go读写锁是写优先的,因此得用readerCount
和readerWait
对读协程进行分类,还将readerCount变成很大的负数来标识已加写锁了。只需记住这几点,我们就能大致推导出读写锁的实现过程,而无需记忆具体的源码细节,这更锻炼我们的业务处理能力!
转载自:https://juejin.cn/post/7213307533280510012