sync.RWMutex
版本: go1.20.3 darwin/arm64
下面有四个场景
- 写操作如何防止写操作?
直接加互斥锁
- 写操作是如何阻止读操作的?
RWMutex.readerCount
是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1
进行写操作时候,会将RWMutex.readerCount
变成负值
- 读操作是如何阻止写操作的?
每次读的时候,会将RWMutex.readerCount
减一,当RWMutex.readerCount变为0后,进行写操作
- 写操作会不会饿死?
首先解释一下为什么可能有饿死的情况发生:写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。
当写操作来的时候,会把RWMutex.readerCount
值拷贝到RWMutex.readerWait
中,然后每次进行读操作RWMutex.readerCount
和RWMutex.readerWait
都会减1,当readerwait=0时,开始写操作
解析
type RWMutex struct {
w Mutex // 互斥锁
writerSem uint32 // 写操作等待读操作完成的信号量
readerSem uint32 // 读操作等待写操作完成的信号量
readerCount atomic.Int32 // 读锁计数器
readerWait atomic.Int32 // 获取写锁时当前需要等待的读锁释放数量
}
// 最大只支持 1 << 30 个读锁
const rwmutexMaxReaders = 1 << 30
信号量(semaphore)
- 获取(acquire,又称 wait、decrement 或者 P)
- 释放(release,又称 signal、increment 或者 V)
获取操作把信号量减一,如果减一的结果是非负数,那么线程可以继续执行。如果结果是负数,那么线程将会被阻塞,除非有其它线程把信号量增加回非负数,该线程才有可能恢复运行)。
释放操作把信号量加一,如果当前有被阻塞的线程,那么它们其中一个会被唤醒,恢复执行。
Go 语言的运行时提供了 runtime_SemacquireMutex
和 runtime_Semrelease
函数,像 sync.RWMutex 这些对象的实现会用到这两个函数。
写锁加锁 Lock()
func (rw *RWMutex) Lock() {
// 竞态检测
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// 1.使用 Mutex 锁,解决与其他写者的竞争
rw.w.Lock()
// 2.判断当前是否存在读锁:先通过原子操作改变readerCount(readerCount-rwmutexMaxReaders),
// 使其变为负数,告诉 RUnLock 当前存在写锁等待;
// 然后再加回 rwmutexMaxReaders 并赋给r,若r仍然不为0, 代表当前还有读锁
r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
// 3.如果仍然有其他 Goroutine 持有互斥锁的读锁(r != 0)
// 会先将 readerCount 的值加到 readerWait中,防止源源不断的读者进来导致写锁饿死,
// 然后该 Goroutine 会调用 sync.runtime_SemacquireMutex 进入休眠状态,
// 并等待所有读锁所有者执行结束后释放 writerSem 信号量将当前协程唤醒。
if r != 0 && rw.readerWait.Add(r) != 0 {
runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
}
// 竞态检测
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
首先会加互斥锁,然后会检查是否有其他的读锁,检查方式就是readerCount原子操作-rwmutexMaxReaders,这个时候readerCount变负的阻止写,然后这个值再补回+rwmutexMaxReaders得到r,如果r!=0,说明此时有其他的读锁,那么为了防止写锁饿死,就将waitCount用原子操作+r,然后调用信号量休眠该goroutine
写锁释放 UnLock()
func (rw *RWMutex) Unlock() {
// 竞态检测
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
// Announce to readers there is no active writer.
// r现在就是读锁的数量
r := rw.readerCount.Add(rwmutexMaxReaders)
// 若超过读锁的最大限制, 触发panic
if r >= rwmutexMaxReaders {
race.Enable()
fatal("sync: Unlock of unlocked RWMutex")
}
// Unblock blocked readers, if any.
// 逐个调用信号量唤醒goroutine
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// Allow other writers to proceed.
// 解锁
rw.w.Unlock()
// 竞态检测
if race.Enabled {
race.Enable()
}
}
没什么好讲的,直接看注释就可以了
就是给readerCount加上max值,然后循环采用信号量的方式唤醒coroutine,最后释放互斥锁
读锁加锁 RLock()
func (rw *RWMutex) RLock() {
// 是否开启检测race
if race.Enabled {
_ = rw.w.state
race.Disable()
}
//这里分两种情况:
// 1.此时无写锁 (readerCount + 1) > 0,那么可以上读锁, 并且readerCount原子加1(读锁可重入[只要匹配了释放次数就行])
// 2.此时有写锁 (readerCount + 1) < 0,所以通过readerSem读信号量, 使读操作睡眠等待
if rw.readerCount.Add(1) < 0 {
// A writer is pending, wait for it.
// 当前有个写锁, 读操作需要阻塞等待写锁释放;
// 其实做的事情是将 goroutine 排到G队列的后面,挂起 goroutine
runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
直接readerCount+1,然后<0说明此时有写锁,调用信号量睡眠
读锁释放 RUnlock()
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
// 写锁等待状态,检查当前是否可以进行获取;
// 首先将 readerCount 减1并赋予r,然后分两种情况判断
// 1.若r大于等于0,读锁直接解锁成功,直接结束本次操作;
// 2.若r小于0,有一个正在执行的写操作,在这时会调用sync.RWMutex.rUnlockSlow 方法;
if r := rw.readerCount.Add(-1); r < 0 {
// Outlined slow-path to allow the fast-path to be inlined
rw.rUnlockSlow(r)
}
if race.Enabled {
race.Enable()
}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
// r + 1 == 0 表示本来就没读锁, 直接执行RUnlock()
// r + 1 == -rwmutexMaxReaders 表示执行Lock()再执行RUnlock()
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
fatal("sync: RUnlock of unlocked RWMutex")
}
// A writer is pending.
// 如果当前有写锁等待,则减少一个readerWait的数目
if rw.readerWait.Add(-1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
先检查是否直接执行RUnlock()||执行Lock()再执行RUnlock(),然后给waitCount-1,与0比较,如果==0,就调用信号量唤醒goroutine
参考
https://dongxiem.github.io/2020/06/07/golang-sync-bao-yuan-ma-pou-xi-2-sync.rwmutex/