必知必会系列-sync.RWMutex

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满足该条件则阻塞等待:
  1. r!=0说明还有其他的读者没有执行释放锁
  2. 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 为负值,会阻塞等待

为什么在读多写少的场景下很快?

当不存在写请求时,读者的加锁和解锁仅仅需要对一个变量的原子加减,没有任何的阻塞等待的动作。

反过来讲,如果在读少写多的情况下性能反而会劣化,因为在互斥锁的基础上额外还多了读写锁的操作

相关推荐
hummhumm1 小时前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
man20173 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
hlsd#3 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@3 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
幸运小圣3 小时前
Vue3 -- 项目配置之stylelint【企业级项目配置保姆级教程3】
开发语言·后端·rust
前端SkyRain4 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁4 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
老猿讲编程5 小时前
Rust编写的贪吃蛇小游戏源代码解读
开发语言·后端·rust
黄小耶@5 小时前
python如何使用Rabbitmq
分布式·后端·python·rabbitmq
宅小海6 小时前
Scala-List列表
开发语言·后端·scala