开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 20 天,点击查看活动详情
RWMutex
RWMutex就是读写锁,其中读读是不互斥的,写读、读写、写写都是互斥的。根据读和写的优先级分为三种:
- Read-preferring:读优先,只要有读锁,新来的读可以直接读,写要一直等到所有读完成后才开始,并发性高,可能会导致写饥饿。
- Write-preferring:写优先,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。
- 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。
Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。
源码阅读
type RWMutex struct {
w Mutex // 互斥锁解决多个writer的竞争
writerSem uint32 // writer信号量
readerSem uint32 // reader信号量
readerCount int32 // reader的数量
readerWait int32 // writer等待完成的reader的数量
}
const rwmutexMaxReaders = 1 << 30
- w:为 writer 的互斥锁;
- readerCount:记录当前 reader 的数量(以及是否有 writer 竞争锁);
- readerWait:记录 writer 请求锁时需要等待 read 完成的 reader 的数量;
- writerSem 和 readerSem:都是为了阻塞设计的信号量。
读锁
- RLock:readerCount加一,如果有writer,就阻塞,等待唤醒后获得读锁
- RUnlock:readerCount减一,如果有writer,唤醒writer
func (rw *RWMutex) RLock() {
// readerCount加一,rw.readerCount<0,此时有writer等待请求锁
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 阻塞
runtime_SemacquireMutex(&rw.readerSem, false, 0)
// 唤醒后获得读锁
}
}
func (rw *RWMutex) RUnlock() {
// readerCount加一减一
if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
rw.rUnlockSlow(r) // 唤醒等待的writer
}
}
写锁
- Lock:获取w锁,反转readerCount代表有writer,如果有reader就先阻塞,等待唤醒获得写锁
- Unlock:反转readerCount代表没有writer,唤醒所有的reader
func (rw *RWMutex) Lock() {
// 保证写的原子性
rw.w.Lock()
// 反转readerCount,告诉reader有writer竞争锁
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// 如果当前有reader持有锁,那么需要等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
// 阻塞
runtime_SemacquireMutex(&rw.writerSem, false, 0)
// 唤醒后获得写锁
}
}
func (rw *RWMutex) Unlock() {
// 告诉reader没有活跃的writer了
r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
// 唤醒阻塞的reader们
for i := 0; i < int(r); i++ {
runtime_Semrelease(&rw.readerSem, false, 0)
}
// 释放内部的互斥锁
rw.w.Unlock()
}
Trylock
- TryRLock:有writer直接返回false,反之readerCount+1,获得读锁
- TryLock:有writer或者reader直接返回false,否则获得写锁
func (rw *RWMutex) TryRLock() bool {
for {
c := atomic.LoadInt32(&rw.readerCount)
// 有writer,直接false
if c < 0 {
return false
}
// CAS增加readerCount,获得读锁
if atomic.CompareAndSwapInt32(&rw.readerCount, c, c+1) {
return true
}
}
}
func (rw *RWMutex) TryLock() bool {
// 读锁都不能获得,直接false
if !rw.w.TryLock() {
return false
}
if !atomic.CompareAndSwapInt32(&rw.readerCount, 0, -rwmutexMaxReaders) {
rw.w.Unlock()
return false
}
return true
}
总结
总体来说,复用mutex保证了写写的互斥,利用readercount记录reader数量和是否有writer,通过这些变量的比较,实现了一个写优先的读写锁,对读多写少的场景下应该有不错的性能。