大家好,我是Anthony_4926。
欢迎访问飞书版本:cnl25x1hkc.feishu.cn/docx/Z6KtdW...
我们先明确一些概念
RWMutex涉及到两个操作:读操作、写操作。请严格区分操作 与锁的概念。
在接下来的代码解析过程中,可以发现只有写操作是真正的会加锁,而读操作是不加锁的。
还需要区分写操作拥有锁 ,和拥有写权限的区别。再次强调只有写操作会加锁,但是,即便是写操作拥有了锁,还需要根据当前是否有读操作正在进行读,来判断写操作是否具有写权限。
读操作会阻塞写操作,使之无法获得写权限。
可能上边的内容会让你有点懵,没关系,向下看就好。
锁结构
go
type RWMutex struct {
w Mutex // 互斥锁,控制多个写入操作的并发执行
writerSem uint32 // 写入操作的信号量
readerSem uint32 // 读操作的信号量
readerCount int32 // 当前读操作的个数
readerWait int32 // 当前写入操作需要等待读操作解锁的个数
}
-
w是用来给给读写锁中的写操作申请锁用的,读操作并不会上锁,只是会阻塞写锁写而已。
-
接下来是两个信号量。为了便于理解,我们将加锁过程简单化,认为协程加锁不成功就会被阻塞,休眠。
- writerSem:如果写操作加锁不成功,就会将写操作休眠,加入与writerSem关联的休眠队列,等writerSem改变时,如果满足条件,则唤醒一个休眠队列中的写操作。
- readerSem:如果读操作加锁不成功,就会将读操作休眠,加入到与readerSem关联的休眠队列,等readerSem改变时,如果满足条件,则唤醒休眠的读操作。
-
最后两个是读操作计数,只不过记录的内容不一样。
- readerCount:记录的是当前读操作的个数
- readerWait:记录的是当前写操作需要等待读操作的个数。如上图,写操作得等待它上边的是哪个读操作结束。需要注意的是,同一时间只能有一个写操作来抢锁。
获取写锁
scss
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(),于是变成右边
第一个写操作先获得锁,此时就会阻塞其他写锁,至于能否获得写操权限,还得看是否有读锁需要等待。
Lock()将readerCount设置为极小的值,是为了标记当前有写操作,已经拥有锁,等待获得写权限。再加回来,是为了获得readerCount的数量。接下来就是判断能否获得写操作权限的逻辑。
如果readerCount不等于0,说明之前有读操作,那就应该将readerWait加上r 表示当前写操作需要休眠,等待readerWait个读操作后被唤醒,获得写权限
获取读锁
scss
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进行关联。
当前状态可以是下图这样
释放读锁
scss
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方法。
释放写锁
scss
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次。还是用下边这个图解释一下