RWMutex
设计思想
Write-preferring:写优先,如果有一个writer在等待写锁,那么会阻塞后面的读请求,优先保障writer
RWMutex采取的就是写优先思想,一个阻塞的Lock方法调用会阻止后面的RLock方法调用
源码解读
读锁
/*
写写靠mutex进行阻塞,读写靠两个信号量进行阻塞
*/
type RWMutex struct {
w Mutex // 用于写锁互斥
writerSem uint32 // 用于writer等待reader的信号量
readerSem uint32 // 用于reader等待writer的信号量
readerCount int32 // 当前reader数
readerWait int32 // writer等待完成的reader数
}
const rwmutexMaxReaders = 1 << 30
func (rw *RWMutex) RLock() {
if atomic.AddInt32(&rw.readerCount, 1) < 0 { //reader数+1,如果+1后的值小于0说明有writer在等待或者持有锁,那么阻塞reader
runtime_SemacquireMutex(&rw.readerSem, false, 0)
}
}
func (rw *RWMutex) RUnlock() {
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { //reader数-1,如果-1后的值小于0说明有writer在等待锁
// 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 { //writer等待的reader数-1,如果等于0那么唤醒writer
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int):
SemacquireMutex 与 Semacquire 类似,但用于分析竞争的互斥体。
如果 lifo 为 true,则将 waiter 排在等待队列的头部。
skipframes 是跟踪期间要省略的帧数,从runtime_SemacquireMutex 的调用者开始
唤醒后从调用方法头开始执行,skipframes是跳过的帧数
runtime_Semacquire(s uint32):
获取信号量, s原子递减1
如果s大于0那么继续执行,否则阻塞等待直到*s>0
唤醒后从阻塞的地方继续执行
写锁
func (rw *RWMutex) Lock() {
rw.w.Lock() //加写锁
// readerCount减readers的最大数量,对readerCount进行反转,用于读锁判断是否有写操作
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 { //当前有reader,那么将reader数设置为readerWait即writer等待的reader数,阻塞writer
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
}
func (rw *RWMutex) Unlock() {
// readerCount加readers的最大数量,获取当前reader数
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// 唤醒阻塞的reader
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 释放写锁,唤醒阻塞的writer
rw.w.Unlock()
}
总结
- 如果读多写少,那么大部分的读都只需要执行一次原子的Add操作就可以拿到锁不需要阻塞,相较于Mutex来说,效率更高
- 如果读少写多,那么大部分写除了要加Mutex的锁外,还要反转设置readerCount的值,判断readerWait的值,效率会一些
- 如果读写差不多,使用RWMutex读还是可以共同执行的只需要执行原子Add操作,RWMutex的效率高于Mutex
常见踩坑
不可复制
RWMutex也是具有状态的,复制后是一个新的RWMutex,原来的RWMutex状态发生变化,复制的RWMutex也不会有变化
可以借助vet工具检查
重入导致死锁
- 第一种场景
内置写锁Mutex的重入
func MutexReentrant() {
var rm sync.RWMutex
rm.Lock()
rm.Lock()
rm.Unlock()
rm.Unlock()
}
- 第二种场景
有活跃的Reader,writer会等待活跃的Reader,活跃的Reader再调用Lock方法,那么活跃的Reader又会等待writer,形成环路等待
func MutexReentrant2() {
var rm sync.RWMutex
rm.RLock()
go func() {
rm.Lock()
time.Sleep(2 * time.Millisecond)
rm.Unlock()
}()
time.Sleep(1 * time.Millisecond)
rm.Lock()
rm.Unlock()
rm.RUnlock()
}
- 第三种场景
writer依赖活跃的reader -> 活跃的reader依赖新来的reader -> 新来的reader依赖writer
比较复杂,一般不会出现
RLock、RUnLock不成对,Lock、UnLock不成对
RLock、RUnLock一定要成对
Lock、UnLock一定要成对