读写锁
GoLang sync.RWLock 读写锁实现分析,宏观上读写锁具有的功能如下:
读锁 | 写锁 | |
---|---|---|
读锁 | ✅ | ❌ |
写锁 | ❌ | ❌ |
总结: 读锁加读锁,写锁全拒绝
怎么实现
如果是你,你会怎样设计,请将想法在评论区留言?如果是我,我会这样想:
-
定义读操作计数readerCount,锁定加一,解锁减一。就是对readerCount进行PV操作
-
定义写操作计数writerCount,锁定加一,解锁减一。就是对writerCount进行PV操作
-
readerCount 有区间范围 [0, 1<<32)
-
writerCount 有区间范围 [0, 1)
-
加读锁检查写锁计数,有值则阻塞读操作,否则放行
-
加写锁检查写锁和读锁计数,有值则阻塞写操作,否则放行
读写锁定义
type RWMutex struct {
w Mutex # held if there are pending writers
writerSem uint32 # semaphore for writers to wait for completing readers
readerSem uint32 # semaphore for readers to wait for completing writers
readerCount int32 # number of pending readers
readerWait int32 # number of departing readers
}
const rwmutexMaxReaders = 1 << 30
属性 | 说明 |
---|---|
w | 互斥锁,保护写操作 |
writerSem | 写操作信号量,用于阻塞或唤醒goroutine |
readerSem | 读操作信号量, 用于阻塞或唤醒goroutine |
readerCount | 读操作计数 |
readerWait | 写锁锁定时,快照当前读操作计数 |
rwmutexMaxReaders | 读操作上限 |
写锁锁定
#注意: race 用于竞态分析 比如 go run -race main.go
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
#通过互斥锁保护写操作,若已存在写锁,那么运行到这里时阻塞
rw.w.Lock()
#写锁时检查读操作计数rw.readerWait,
#
# r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
#
#那么r就是运行前的rw.readerWait且r >= 0,
#
# rw.readerCount 变成负数
#
#也就表明当rw.readerCount为负数时,存在写操作等待
#
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
# r = 0, 表示没有读锁,可以执行写操作
# r != 0, 存在读操作,那么阻塞写操作,同时记录待执行读操作的数量
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
#
#runtime_SemacquireMutex runtime库提供
#
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
读锁锁定
#注意: race 用于竞态分析 比如 go run -race main.go
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
#
# atomic.AddInt32(&rw.readerCount, 1) 即
#
# rw.readerCount = rw.readerCount + 1
#
# rw.readerCount > 0 无写操作,都是度操作
#
# rw.readerCount < 0 存在申请写操作, 因为在写锁操作中将 rw.readerCount 设置为负数, 并阻塞读操作
#
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
#
#runtime_SemacquireMutex runtime库提供
#
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
写锁解锁
func (rw *RWMutex) Unlock() {
if race.Enabled {
_ = rw.w.state
race.Release(unsafe.Pointer(&rw.readerSem))
race.Disable()
}
#
# r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
#
# 将rw.readerCount 设置成正数,恢复读锁锁定
#
#GoLang 将rw.readerCount 正负区分读写操作,实在是妙
#
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
#
#超过区间范围,报错
#
#例如:
# 未写锁锁定,直接写锁解锁,那么r = rwmutexMaxReaders
# 读锁锁定,直接写锁解锁,那么r > rwmutexMaxReaders
if r >= rwmutexMaxReaders {
race.Enable()
throw("sync: Unlock of unlocked RWMutex")
}
#
#唤醒全部被阻塞的读操作
#
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
#
# 释放互斥锁,允许下一个写操作
#
rw.w.Unlock()
if race.Enabled {
race.Enable()
}
}
读锁解锁
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
#
# r := atomic.AddInt32(&rw.readerCount, -1)
#
# 若无写锁,那么 r >= 0 ,直接放行,称之为快路径
#
# 若有写锁,那么 r < 0, 进入慢路径
#
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
rw.rUnlockSlow(r)
}
if race.Enabled {
race.Enable()
}
}
#
# r < 0
#
# r + 1 = 0:
# (1)意味着读操作数量已超过设定值。写锁锁定将rw.readerCount - rwmutexMaxReaders, 意味rw.readerCount 将会从-rwmutexMaxReaders增长到-1
#
# (2)未读锁锁定,直接读锁解锁
#
# r + 1 = -rwmutexMaxReaders:
# (1)极限情况读操作数量rwmutexMaxReaders时申请写操作,那么rw.readerCount = rwmutexMaxReaders - rwmutexMaxReaders = 0
# 在写锁未解锁前,读操作陆续解锁,即rw.readerCount--,直到rw.readerCount = -rwmutexMaxReaders。若继续解锁,那么报错
#
#
func (rw *RWMutex) rUnlockSlow(r int32) {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
throw("sync: RUnlock of unlocked RWMutex")
}
#
# rw.readerWait 是申请写操锁时当前待执行读操作的数量
#
# rw.readerWait = 0 表示已无操作,可以唤醒写操作
#
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
总结
GoLang sync.RWLock 基于sync.Mutex实现,逻辑清晰,简化实现。 sync.Mutex实现相对复杂,下期分享