go RWmutex读写锁原理

所谓读写锁RWMutex,完整的表述应该是读写互斥锁,可以说是Mutex的一个改进版,在某些场景下可以发挥更加灵活的控制能力,比如:读取数据频率远远大于写数据频率的场景。

比如,程序钟写操作少而读操作多,简单的说,如果执行过程时1次写然后N次读的话,使用Mutex这个过程将是串行的, 因为即便N次读操作相互之间并不影响,但也都需要持有Mutex后才可以操作。 如果使用(读写锁)RWmutex,多个读操作时可以同时持有锁,并发能力将大大提升。

实现读写锁需解决如下几个问题

  1. 写锁需要阻塞写锁:就是说某一时刻只允许有一个协程拥有写锁,一个协程拥有写锁时,其他协程写锁定需要阻塞。
  2. 写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞,写的时候不允许任何其他协程读操作。
  3. 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻,一个协程对资源进行读操作时,其他写锁必须阻塞。
  4. 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁。

读写锁数据结构

源码包 src/sync/rwmutex.go:RWMutex 定义了读写锁数据结构:

type RWMutex struct {
    w           Mutex  //用于控制多个写锁,获得写锁首先要获取该锁,
    				   //如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
    writerSem   uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
    readerSem   uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
    readerCount int32  //记录读者个数
    readerWait  int32  //记录写阻塞时读者个数
}

据结构可见,读写锁内部仍有一个互斥锁,用于将两个写操作隔离开来,其他的几个都用于隔离读操作和写操作。

RWMutex提供4个简单的接口来提供服务:

  • Rlock():读锁定
  • RUnlock():解除读锁定
  • Lock(): 写锁定,与Mutex完全一致
  • Unlock():解除写锁定,与Mutex完全一致

lock()实现逻辑

写锁操作需要做的两件事:

  • 获取互斥锁
  • 阻塞等待所有读操作结束(如果有的话)

lock接口实现流程:
请添加图片描述

Unlock()实现逻辑

接触写锁定要做两件事:

  • 唤醒因读锁而被阻塞的写成(如果有的话)
  • 解除互斥锁

unlock 接口实现流程
请添加图片描述

具体例子

package main

import (
    "fmt"
    "sync"
)
var count int
var mutex sync.Mutex

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mutex.Lock()
            count++
            mutex.Unlock()
        }()
    }
    wg.Wait()
    fmt.Println(count)
}

上面的代码演示了如何使用互斥锁保护一个共享变量count,并发地对其进行加一操作。需要注意的是,在使用互斥锁时,需要在临界区获取锁并在临界区结束时释放锁。

RLock()实现逻辑

读锁定需要做两件事:

  • 增加读操作计数,即readerCount++
  • 阻塞等待写操作结束(如果有的话)
    请添加图片描述

RUnlock()实现逻辑

解除读锁定需要做两件事:

  • 减少读操作计数,即readerCount–
  • 唤醒等待写操作的协程(如果有的话)
    请添加图片描述
    注意:即便有协程阻塞等待写操作,并不是所有的解除读锁定操作都会唤醒该协程,而是最后一个解除读锁定的协程才会释放信号量将该协程唤醒,因为只有当所有读操作的协程释放锁后才可以唤醒协程。
package main
import (
    "fmt"
    "sync"
)
var count int
var rwMutex sync.RWMutex
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            rwMutex.RLock()
            fmt.Println(count)
            rwMutex.RUnlock()
        }()
    }
    wg.Add(1)
    go func() {
        defer wg.Done()
        rwMutex.Lock()
        count++
        rwMutex.Unlock()
    }()
    wg.Wait()
}

场景分析

写操作是如何阻止写操作

读写锁包含一个互斥锁(Mutex),写锁定必须要先获取该互斥锁,如果互斥锁已被协程A获取(或者协程A在阻塞等待读结束),意味着协程A获取了互斥锁,那么协程B只能阻塞等待该互斥锁。
所以,写操作依赖互斥锁阻止其他的写操作。

写操作是如何阻止读操作

我们知道RWMutex.readerCount是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1,所以readerCount取值为[0, N],N为读者个数,实际上最大可支持2^30个并发读者。
当写锁定进行时,会先将readerCount减去230,从而readerCount变成了负值,此时再有读锁定到来时检测到readerCount为负值,便知道有写操作在进行,只好阻塞等待。而真实的读操作个数并不会丢失,只需要将readerCount加上230即可获得。
所以,写操作将readerCount变成负值来阻止读操作的。

读操作是如何阻止写操作的

读锁定会先将RWMutext.readerCount加1,此时写操作到来时发现读者数量不为0,会阻塞等待所有读操作结束。
所以,读操作通过readerCount来将来阻止写操作的。

为什么写锁不会被饿死

我们知道,写操作要等待读操作结束后才可以获得锁,写操作等待期间可能还有新的读操作持续到来,如果写操作等待所有读操作结束,很可能被饿死。然而,通过RWMutex.readerWait可完美解决这个问题。

写操作到来时,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait值,当RWMutex.readerWait值变为0时唤醒写操作。

所以,写操作就相当于把一段连续的读操作划分成两部分,前面的读操作结束后唤醒写操作,写操作结束后唤醒后面的读操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值