首页 > 其他分享 >读写锁的原理

读写锁的原理

时间:2022-08-22 11:46:48浏览次数:58  
标签:协程 写锁 读写 阻塞 读锁 线程 操作 原理

读写锁的相关知识

读写锁是写独占,读共享,若有一个线程正在写,占了写锁,其他线程写锁读锁都拿不到。
读写锁高2字节保存读锁,低2字节保存写锁。

1 、如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样可以有多个线程并行操作。但是一旦加了读锁,写锁就加不了了。而且获取到读锁,还是可以修改被保护的变量的。
2、一个线程先要写操作,获取写锁必须读锁和写锁都未被占用才可以成功加写锁。因此如果读线程很多,一直占用读锁,读锁的计数值很大,写锁很久都获取不到,导致写锁饥饿。

为了解决写锁饥饿,如果要获取写锁(读锁计数不为0,需要等待所有读锁释放 ),为了防止其他线程不断获取读锁,如果要获取写锁阻塞了,那么读锁也获取不到。简而言之就是如果写锁获取不到,读锁也不能获取。

1.前言

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

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

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

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

按照这个思路,即读写锁如何解决这些问题的,来分析读写锁的实现。

读写锁基于Mutex实现,实现源码非常简单和简洁,又有一定的技巧在里面。

2.读写锁数据结构

2.1 类型定义

源码包src/sync/rwmutex.go:RWMutex定义了读写锁数据结构:(一个锁两个信号量两个数目)

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

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

简单看下RWMutex提供的4个接口,后面再根据使用场景具体分析这几个成员是如何配合工作的。

2.2 接口定义

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

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

2.2.1 Lock()实现逻辑

写锁操作需要做两件事:

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

lock接口实现流程如下:

2.2.2 Unlock()实现逻辑

解除写锁定要做两件事:

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

所以func (rw *RWMutex) Unlock()接口实现流程如下图所示:

2.2.3 RLock()实现逻辑

读锁定需要做两件事:

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

接口实现流程如下图所示:

2.2.4 RUnlock()实现逻辑

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

  • 减少读操作计数,即readerCount–
  • 唤醒等待写操作的协程(如果有的话)

接口实现流程如下图所示:

注意:即便有协程阻塞等待写操作,并不是所有的解除读锁定操作都会唤醒该协程,而是最后一个解除读锁定的协程才会释放信号量将该协程唤醒,因为只有当所有读操作的协程释放锁后才可以唤醒协程。

场景分析

上面我们简单看了下4个接口实现原理,接下来我们看一下是如何解决前面提到的几个问题的。

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

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

所以,写操作依赖互斥锁阻止其他的写操作。

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

这个是读写锁实现中最精华的技巧。

我们知道RWMutex.readerCount是个整型值,用于表示读者数量,不考虑写操作的情况下,每次读锁定将该值+1,每次解除读锁定将该值-1,所以readerCount取值为[0, N],N为读者个数,实际上最大可支持2^30个并发读者。

当写锁定进行时,会先将readerCount减去230,从而readerCount变成了负值,此时再有读锁定到来时检测到readerCount为负值,便知道有写操作在进行,只好阻塞等待。而真实的读操作个数并不会丢失,只需要将readerCount加上230即可获得。

所以,写操作将readerCount变成负值来阻止读操作的。

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

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

所以,读操作通过readerCount来将来阻止写操作的。

3.4 为什么写锁定不会被饿死

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

写操作到来时,会把RWMutex.readerCount值拷贝到RWMutex.readerWait中,用于标记排在写操作前面的读者个数

前面的读操作结束后,除了会递减RWMutex.readerCount,还会递减RWMutex.readerWait值,当RWMutex.readerWait值变为0时唤醒写操作。

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


另外,根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」。

读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。

而写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。

读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程「饥饿」的现象。

写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被「饿死」。

既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏袒任何一方,搞个「公平读写锁」。

公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。


标签:协程,写锁,读写,阻塞,读锁,线程,操作,原理
From: https://www.cnblogs.com/success-zh/p/16612294.html

相关文章

  • 容斥原理
    时间复杂度分析:O(2^n)所有一项集合的个数-两项的集合个数+所有三项的集合个数-四项集合的个数......;C(n,1)+C(n,2)+C(n,3)+......+C(n,n);又因为:C(n,0)+C(n,1)+C(n,2)+C(......
  • 深入理解 Spring 事务:入门、使用、原理
    大家好,我是树哥。Spring事务是复杂一致性业务必备的知识点,掌握好Spring事务可以让我们写出更好地代码。这篇文章我们将介绍Spring事务的诞生背景,从而让我们可以更清......
  • 微服务治理热门技术揭秘:动态读写分离
      我们从应用的视角出发整理抽象了我们在访问、使用数据库时场景的一些稳定性治理、性能优化、提效等方面的实战经验,对于每一个后端应用来说,数据库无疑是重中之重,我们......
  • mycat读写分离、mysql主从的安装
    数据库安装手册目录数据库安装手册1、数据库安装1.1环境准备1.1.1关闭selinux1.1.2修改主机名1.1.3域名解析1.1.3时间同步1.2mysql安装1.2.1二进制包上传至服务器......
  • 2022.8.21 读写锁与阻塞队列
    9、读写锁   自定义的缓存,没有加锁,就会出现一个没有写入完成,另一个突然插进来的情况 packagecom.xing.rw; ​ importjava.util.HashMap; importjava.util.......
  • redis核心数据结构与高性能原理
    一:redis安装1.下载wgethttp://download.redis.io/releases/redis-5.0.3.tar.gz 2.解压和编译tarxzfredis‐5.0.3.tar.gzcdredis‐5.0.3#进入到解压好的re......
  • go 语言 chan读写数据
    示例demo51packagemainimport("fmt""time")funcsendData(chchanint){//把数据写到通道里fori:=0;i<20;i++{......
  • 容斥原理
    https://www.acwing.com/problem/content/description/892/给定一个整数\(n\)和\(m\)个不同的质数\(p_1,p_2,...,p_m\)。请你求出1∼\(n\)中能被\(p_1,p_......
  • 一文搞懂 Ftrace 的实现原理
    arm64栈帧结构arm64有31个通用寄存器r0-r30,用法分别如下:寄存器意义SPStackPointer:栈指针r30LinkRegister:在调用函数时候,保存下一条要执行指令的......
  • Hadoop及其三大组件原理
    Hadoop是什么?由Apache基金会开发的分布式系统基础架构海量数据的存储和分析计算 Hadoop架构历史:1.0HDFS和MapReduce2.0在1.0基础上增加了YARN(任务调度),解放了Ma......