sync.RWMutex 在某一时刻只能由任意数量的reader持有,或者是只被单个的writer持有
即读操作可并发重入,写操作是互斥的
-
优化读多写少的场景 读锁请求无需获取互斥锁
-
采用FIFO的设计思想,一旦有写者尝试获取锁,后续的写者和读者都会被阻塞,防止写锁饥饿
-
使用信号量实现阻塞与唤醒语义
构成
- w 互斥锁
- writerSem pending写者持有的信号量
- readerSem pending读者持有的信号量
- readerCount
number of pending readers
记录读者的个数 - readerWait
number of departing readers
记录写阻塞时读者的个数
go
// There is a modified copy of this file in runtime/rwmutex.go.
// If you make any changes here, see if you should make them there.
// A RWMutex is a reader/writer mutual exclusion lock.
// The lock can be held by an arbitrary number of readers or a single writer.
// The zero value for a RWMutex is an unlocked mutex.
//
// A RWMutex must not be copied after first use.
//
// If a goroutine holds a RWMutex for reading and another goroutine might
// call Lock, no goroutine should expect to be able to acquire a read lock
// until the initial read lock is released. In particular, this prohibits
// recursive read locking. This is to ensure that the lock eventually becomes
// available; a blocked Lock call excludes new readers from acquiring the
// lock.
//
// In the terminology of the Go memory model,
// the n'th call to Unlock "synchronizes before" the m'th call to Lock
// for any n < m, just as for Mutex.
// For any call to RLock, there exists an n such that
// the n'th call to Unlock "synchronizes before" that call to RLock,
// and the corresponding call to RUnlock "synchronizes before"
// the n+1'th call to Lock.
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
}
核心实现
读写锁设计方案
基于对读操作和写操作的优先级,读写锁的设计和实现可以分成三类:
- Read-preferring:读优先的设计可以提供很高的并发性,但是在竞争激烈的情况下可能会导致写饥饿。
- Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。
- 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效。
Go 标准库中的 RWMutex 设计是 Write-preferring 方案,一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。
获取读锁
- 对该变量
readerCount
尝试+1 如果得到的值小于0,说明该锁被写者占据,则阻塞在信号量readerSem上,等待被唤醒 - 其中
runtime_SemacquireMutex
函数的作用是以阻塞方式等待信号量的值(rw.readerSem)大于0,然后将该值减1
go
// RLock locks rw for reading.
// 不应该递归的尝试获取读锁
// It should not be used for recursive read locking; a blocked Lock
// call excludes new readers from acquiring the lock. See the
// documentation on the RWMutex type.
func (rw *RWMutex) RLock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// A writer is pending, wait for it.
runtime_SemacquireMutex(&rw.readerSem, false , 0 )
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
}
}
释放读锁
- 对该变量
readerCount
尝试-1 如果得到的值小于0,说明存在pending的写者,需要额外处理: - 如果
rw.readerWait-1=0
说明最后一个读者也已经释放锁,则这时候需要唤醒阻塞的写者
go
// RUnlock undoes a single RLock call;
// it does not affect other simultaneous readers.
// It is a run-time error if rw is not locked for reading
// on entry to RUnlock.
func (rw *RWMutex) RUnlock() {
if race.Enabled {
_ = rw.w.state
race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
race.Disable()
}
if r := atomic.AddInt32(&rw.readerCount, -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) {
if r+1 == 0 || r+1 == -rwmutexMaxReaders {
race.Enable()
fatal("sync: RUnlock of unlocked RWMutex")
}
// A writer is pending.
if atomic.AddInt32(&rw.readerWait, -1) == 0 {
// The last reader unblocks the writer.
runtime_Semrelease(&rw.writerSem, false, 1)
}
}
获取写锁
- 首先获取互斥锁,其他写者只能阻塞在这把锁上
readerWait
变量减去一个rwmutexMaxReaders
,可以保证这个变量现在一定小于0。其他读者通过判断这个变量小于0便可以知道有一个写者可能处于pending状态。也就是说一旦有写者尝试获取锁,后续的读者/写者都会阻塞r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0
满足该条件则阻塞等待:
r!=0
说明还有其他的读者没有执行释放锁atomic.AddInt32(&rw.readerWait, r) != 0
说明并不是所有的读者都释放了锁,这时候便需要阻塞等待了(读者释放锁的时候,如果观察到rw.readerCount < 0
则知道存在可能pending的写者,会对变量readerWait - 1
,直到该变量等于0,等所有的读者已经释放,就会去唤醒阻塞的写者)
go
// Lock locks rw for writing.
// If the lock is already locked for reading or writing,
// Lock blocks until the lock is available.
func (rw *RWMutex) Lock() {
if race.Enabled {
_ = rw.w.state
race.Disable()
}
// First, resolve competition with other writers.
rw.w.Lock()
// Announce to readers there is a pending writer.
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
// Wait for active readers.
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
runtime_SemacquireMutex(&rw.writerSem, false, 0)
}
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(&rw.readerSem))
race.Acquire(unsafe.Pointer(&rw.writerSem))
}
}
const rwmutexMaxReaders = 1 << 30
释放写锁
- readerCount加上
rwmutexMaxReaders
,表明写者已经释放 - 如果有pending状态的读者请求,全部唤醒之
- 最终释放互斥锁,允许其他写者竞争
scss
// Unlock unlocks rw for writing. It is a run-time error if rw is
// not locked for writing on entry to Unlock.
//
// As with Mutexes, a locked RWMutex is not associated with a particular
// goroutine. One goroutine may RLock (Lock) a RWMutex and then
// arrange for another goroutine to RUnlock (Unlock) it.
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 := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
if r >= rwmutexMaxReaders {
race.Enable()
fatal("sync: Unlock of unlocked RWMutex")
}
// Unblock blocked readers, if any.
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()
}
}
总结
如何保证最多只有一个写操作?
读写锁中包含一个互斥锁 Mutex w,写者必须先获取到此互斥锁,若此互斥锁已被其他协程获取,当前协程会阻塞等待该互斥锁
如何保证存在读操作的时候,写操作被禁止?
RWMutex.readerCount 表示读者数量,不考虑写操作的情况下,每次读锁定会将该值+1,每次解除读锁定会将该值-1。当且仅当所有的读操作都已经释放锁后 写锁才能继续进行,否则会被阻塞
如何防止写锁饥饿?
写者会先将 readerCount 减去 2^30,readerCount 变成了负值,此后再有读者会发现 readerCount 为负值,会阻塞等待
为什么在读多写少的场景下很快?
当不存在写请求时,读者的加锁和解锁仅仅需要对一个变量的原子加减,没有任何的阻塞等待的动作。
反过来讲,如果在读少写多的情况下性能反而会劣化,因为在互斥锁的基础上额外还多了读写锁的操作