Go基础篇:RWMutex实现原理

1、RWMutex是什么

在Go语言标准库中RWMutex是一个reader/write互斥锁,RWMutex它不限制资源的并发读,但读写、写写操作无法并行执行。

Y

N

N

N

举例说明的话,使用10个goroutine进行读操作,每次都sleep 1ms, 另一个goroutine进行写操作,每秒进行一次写操作。因为读操作可以并行执行,写操作时只允许一个线程执行,这就是readers-writers问题。

package main import ( "sync" "time" ) // 一个线程安全的计数器 type Counter struct { mu sync.RWMutex count uint64 } // 使用写保护 func (c *Counter) Incr() { c.mu.Lock() c.count++ c.mu.Unlock() } // 使用读锁保护 func (c *Counter) Count() uint64 { c.mu.RLock() defer c.mu.RUnlock() return c.count } func main() { var counter Counter for i := 0; i < 10; i++ { //10个Reader go func() { for { counter.Count() //计数器读操作 time.Sleep(time.Microsecond) } }() } for { //一个writer counter.Incr() //计数器写操作 time.Sleep(time.Second) } }

上面例子中,Incr 方法会修改计数器的值,在写操作时会使用 Lock/Unlock 进行保护。 Count 方法会读取当前计数器的值,在读操作时会使用 RLock/RUnlock 方法进行保护。

Incr 方法每秒调用一次,在竞争锁的过程中频率还是比较低的,10 个 goroutine 每毫秒会执行一次查找,通过读写锁,可以提高程序的性能,因为可以并发的执行读写。若过使用 Mutex,在多个 reader 并发读的时候需要排队进行获取锁,自然在性能上没有 RWMutex 并发读的性能好。

2、RWMutex实现原理

在Go语言中,RWMutex是基于互斥锁、变量、信号量等并发原语来实现的。Go标准库中的RWMutex是基于Mutex实现的。

2.1、 结构体

RWMutex包括以下5个字段

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 }

其中:

w:复用互斥锁提供的能力

readerCount:记录当前reader的数量(以及是否有writer竞争锁)

readerWait:记录writer请求锁时需要等待reader完成的reader的数量

writerSem和readerSem:都是为了阻塞设计的信号量,分别用于写等待读和读等待写

2.2、 读锁

看下RLock和RUnlock方法

func (rw *RWMutex) RLock() { if atomic.AddInt32(&rw.readerCount, 1) < 0 { // A writer is pending, wait for it. runtime_SemacquireMutex(&rw.readerSem, false, 0) } } func (rw *RWMutex) RUnlock() { if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 { // Outlined slow-path to allow the fast-path to be inlined rw.rUnlockSlow(r) } } func (rw *RWMutex) rUnlockSlow(r int32) { if atomic.AddInt32(&rw.readerWait, -1) == 0 { // The last reader unblocks the writer. runtime_Semrelease(&rw.writerSem, false, 1) } }

1、如果方法atomic.AddInt32返回负数,其他Goroutine获得了写锁,当前Goroutine就会调用runtime_SemacquireMutex陷入休眠等待锁的释放

2、如果方法atomic.AddInt32返回结果为非负数,没有Goroutine获得写锁,当前方法会成功返回

当调用RUnlock时,我们需要将Reader计数减去1(第8行),根据atomic.AddInt32的返回值不同分别进行处理:

(1)返回值是负数:表示有一个或多个正在执行写操作,此时会调用rUnlockSlow方法,检查检查 reader 是不是都释放读锁;

(2)返回值是非负数:读锁直接解锁成功;

所以,rUnlockSlow 将持有锁的 reader 计数减少 1 的时候,会检查既有的 reader 是不是都已经释放了锁,如果都释放了锁,就会唤醒 writer,让 writer 持有锁。

2.3、 写锁

当资源使用者想要获得写锁时,需要调用Lock方法

当一个writer获得了内部的互斥锁,会反转readerCount字段,吧它从原来的正整数readerCount(>=0)修改为负数(readerCount-rwmutexMaxReaders),让这个字段保持两个含义(既保存了reader的数量,又表示当前有writer)

func (rw *RWMutex) Lock() { // 首先解决其他writer竞争问题 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) } }

第3行:调用Lock阻塞后续的写操作;

让readerCount不为0,说明当前有其他的reader持有读锁,RWMutex会把当前的readerCount赋值给readerWrite字段保存下来(第7行),此时writer会阻塞进入等待状态(第8行)

当reader释放读锁时,readerWait就减1,当所有活跃的reader都释放了读锁,才会唤醒这个writer

写锁的释放会调用Unlock方法

当一个 writer 释放锁,会对 readerCount 字段进行反转,减去 rwmutexMaxReaders 变为负数,所以反转方法就是给它增加 rwmutexMaxReaders 这个常数值。

当 writer 要释放锁啦,之后新的 reader,不会再阻塞。

在 RWMutex 的 Unlock 返回之前,需要把内部的互斥锁释放。释放完毕后,其他的 writer 才可以继续竞争这把锁。

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) } // 释放内部互斥锁,让其他读写进行active rw.w.Unlock() }

在 Lock 方法中,先获取内部的互斥锁接着修改其他字段,但在 Unlock 中,却是先修改其他字段接着才会释放内部的互斥锁,这样才能保证字段的修改的同时会受到互斥锁的保护。

小结,读写互斥锁在互斥锁之上提供了额外的更细粒度的控制,能够在读操作远远多于写操作时提升性能。

3、RWMutex常见问题

问题1:不可复制

当读写锁正在使用时,它的字段会有一些状态,这个时候你去复制时,会把其字段对应的状态复制过来。当原来锁释放时,不会修改你复制出来的这个锁,会导致复制的锁不会被释放。

问题2:释放未加锁的RWMutex

在读写锁中,Lock与Unlock的调用是成队出现的,RLock和RUnLock也是,Lock和RLock缺少的调用会导致锁未正确被释放,可能会出现死锁。但是Unlock和RUnlock会导致pani。在生产中是不允许的,切记一定成对出现。

  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值