ToB企服应用市场:ToB评测及商务社交产业平台

标题: go读写锁 [打印本页]

作者: 前进之路    时间: 2024-1-21 22:26
标题: go读写锁
go读写锁

互斥锁每次只让一 g通过,去读写数据。但是读数据操作,并发其实没有问题。所以诞生了 读写锁。
读协程可以并发,一起读。但是 写协程还是要走互斥锁,只能一个个通过。
先加了读锁

先加了读锁。那么写的协程,就需要去休眠队列中等待。一直到读锁都释放。
先加了写锁

这个时候,不管再来 写协程还是读协程,都去休眠队列等待。
小结:
  1.   没有加写锁时,多个协程都可以加读锁
  2.   加了写锁时,无法加读锁,读协程排队等待
  3.   加了读锁,写锁排队等待
复制代码
定义
  1. type RWMutex struct {
  2.         w           Mutex        // held if there are pending writers
  3.         writerSem   uint32       // semaphore for writers to wait for completing readers
  4.         readerSem   uint32       // semaphore for readers to wait for completing writers
  5.         readerCount atomic.Int32 // number of pending readers
  6.         readerWait  atomic.Int32 // number of departing readers
  7. }
  8. w:互斥锁作为写锁
  9. writerSem:作为写协程队列
  10. readerSem:作为读协程队列
  11. readerCount: 正值:正在读的协程 负值:加了写锁
  12. readerWait:写锁应该等待读协程个数
复制代码
有三个sema队列了,w本身底层有一个,readerSem 和 writerSem。
运行逻辑

当锁是一个初始化的状态,来了一个写协程

  1. `rwmutexMaxReaders` 是一个非常大的常量
复制代码
把readerCount 改成了一个 很大的负数
加写锁,有读协程已经在读了

来了写锁后,把 readerCount 也减去了一个很大的数,但是 3还是能从这个值中体现。
但是,当再有 读的g过来时候,发现readerCount 为负数,就会去readSem中休眠。
代码实现

读锁

加锁
  1. func (rw *RWMutex) RLock() {
  2.    
  3.     // 将readerCount+1,发现还是负的,就去休眠,说明有写锁在等待
  4.         if rw.readerCount.Add(1) < 0 {
  5.                 runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)
  6.         }
  7.     // 如果不是负数,则加锁成功,去读数据
  8. }
复制代码
小结:
  1. 将给readerCount无脑加-
  2. 如果readerCount是正数,加锁成功
  3. 如果readerCount是负数,说明被加了写锁,陷入`readerSem`
复制代码
解锁
  1. func (rw *RWMutex) RUnlock() {
  2.         // 将 readerCount 减去1, 如果 r 小于0,说明有写协程 在等待
  3.         if r := rw.readerCount.Add(-1); r < 0 {
  4.                 // Outlined slow-path to allow the fast-path to be inlined
  5.                 rw.rUnlockSlow(r)
  6.         }
  7. }
  8. func (rw *RWMutex) rUnlockSlow(r int32) {
  9.    
  10.     // readerWait  写锁应该等待读协程个数 减去1(自身) 之后,
  11.     //如果是 0,说明没有 读协程在读取数据了 ,就去 `runtime_Semrelease `唤醒换一个写协程
  12.         if rw.readerWait.Add(-1) == 0 {
  13.                 // The last reader unblocks the writer.
  14.                 runtime_Semrelease(&rw.writerSem, false, 1)
  15.         }
  16. }
复制代码
读锁在解锁时候,去判断了 readerCount 是否小于0 是否有写协程在等待;然后再释放写协程,又判断了 readerWait 是否等于0,因为大于0,说明还有读协程。
小结:
  1.   给readerCountit减1
  2.   如果readerCount是正数,解锁成功,没有写协程在排队
  3.   如果readerCount是负数,有写锁在排队
  4.   如果自己是readerWait的最后一个,唤醒写协程
复制代码
问题: 但是读锁在加锁时候,并没有 给 readerWait加值,这里判断是否有效呢 ?
写锁
  1. 加锁
  2. func (rw *RWMutex) Lock() {
  3.         rw.w.Lock() // 先去加互斥锁,加上了再执行下面的逻辑,加不上直接去 w的sema中休眠了
  4.         r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders
  5.     // 判断 r是否为0, 等于0 则直接加锁成功了。
  6.         if r != 0 && rw.readerWait.Add(r) != 0 {
  7.                 runtime_SemacquireRWMutex(&rw.writerSem, false, 0)
  8.         }
  9. }
复制代码
讲下这里不为0的情况,如果r不为0,比如r为2,则说明还有2个读协程在工作。
rw.readerWait.Add(r)这句代码,正好解释了上面的问题。这里给 readerWait 赋值了,所以读协程在解锁时候,判断这个值才有用。
如果不来写协程,那这个 readerWait 没有意义,因为这是判断是否释放写协程的。
那有没有lock()被两个 写协程 先后连续执行,让 r 的值为一个很大的负数?
不会。因为要先去加 互斥锁。一个写协程加上后,其他的写协程只能去 sema中等待。上篇有讲过。 所以上面有一个举例的图其实是有问题的。
小结加写锁:
  1. 先加mutex写锁,若已经被加写锁会阻塞等待
  2. 将readerCount变为负值,阻塞读锁的获取
  3. 计算需要等待多少个读协程释放
  4. 如果需要等待读协程释放,陷入writerSem
复制代码
解写锁
  1. func (rw *RWMutex) Unlock() {
  2.        
  3.         r := rw.readerCount.Add(rwmutexMaxReaders)
  4.     // 去释放读协程,没有去释放 `writerSem`里面的写协程,因为这里面根本不会有休眠的写协程
  5.         for i := 0; i < int(r); i++ {
  6.                 runtime_Semrelease(&rw.readerSem, false, 0) // 释放读协程
  7.         }
  8.         rw.w.Unlock() // 解锁 互斥锁,让互斥锁的sema等待队列中的协程,重新去竞争锁
  9. }
复制代码
小结 解写锁 :
  1.   将readercount变为正值,允许读锁的获取
  2.   释放在readerSem中等待的读协程 上面讲了,这个时候 writerSem 是空的
  3.   解锁mutex
复制代码
适合场景
  1. 适合读多写少的场景,如果都是写的场景,其实和互斥锁一样。
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4