必知必会系列-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 为负值,会阻塞等待

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

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

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

相关推荐
易协同低代码24 分钟前
通达OA核心类库TD类深度解析
后端
Gopher_HBo26 分钟前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志1 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..1 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects1 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰1 小时前
一些基础和问题解决
后端
陈随易2 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN2 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana2 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超2 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构