逐步学习Go-sync.RWMutex(读写锁)-深入理解与实战

概述

在并发编程中,我们经常会遇到多个线程或协程访问共享资源的情况。为了保护这些资源不被同时修改,我们会用到"锁"的概念。

Go中提供了读写锁:sync.RWMutex。
sync.RWMutex是Go语言提供的一个基础同步原语,它是Reader/Writer Mutual Exclusion Lock的缩写,通常被称为"读写锁"。
读写锁允许多个读锁同时拥有者,但在任何时间点只允许一个写锁拥有者,或者没有锁拥有者。

这让读多写少的场景获得了更高的并发性能。

应用场景

  1. 典型应用场景就是读多写少
  2. 一写多读

提供的方法

sync.RWMutex提供了以下方法:

type RWMutex
// 获取写锁,有读锁或者写锁被其他goroutine使用则阻塞等待
func (rw *RWMutex) Lock()
// 尝试获取写锁,获取到则返回true,没有获取到则为false
func (rw *RWMutex) TryLock() bool
// 释放写锁
func (rw *RWMutex) Unlock()
// 获取读锁,
func (rw *RWMutex) RLock()
// 尝试获取读锁,获取到则返回true,没有获取到则为false
func (rw *RWMutex) TryRLock() bool
// 释放读锁
func (rw *RWMutex) RUnlock()// 返回Locker
func (rw *RWMutex) RLocker() Locker

COPY

注意

使用RWMutex的时候,一旦调用了Lock方法,就不能再把该锁复制到其他地方使用,否则可能会出现各种问题。这是由于锁的状态(被哪个协程持有,是否已经被锁定等)是存储在RWMutex的结构体中,如果复制了RWMutex,那么复制后的RWMutex就会有一个全新的状态,锁的行为就会变得不可预测。
RWMutex和Mutex一样,一旦有了Lock调用就不能到处copy了,否则出现各种问题。

源码实现

RWMutex结构体

让我们一起深入Go的源码,看看RWMutex是如何实现的。
RWMutex 的结构体主要包括五个主要的字段,这些字段描述了锁的当前状态和持有者信息:

type RWMutex struct {// Mutex,互斥锁。写者互斥锁,所有的写者加锁都调用w.Lock或者w.TryLockw           Mutex   // 写者信号量。当最后一个读者释放了锁,会触发一个信号通知writerSemwriterSem   uint32  // 读者信号量。当写者释放了锁,会触发一个信号通知readerSemreaderSem   uint32      // readerCount 记录当前持有读锁的协程数量。如果为负数,表示有写者在等待所有读者释放锁。如果为0,表示没有任何协程持有锁readerCount atomic.Int32 // readerWait 记录写者需要等待的读者数量。当一个写者获取了锁之后,readerWait会设置为当前readerCount的值。当读者释放锁时,readerWait会递减1readerWait  atomic.Int32 
}

COPY

读者加锁RLock()

加读锁时非常简单,就是将结构体中的readerCount加1,如果+1后为负数表示有写者等待则等待写者执行完成。

实现代码

func (rw *RWMutex) RLock() {// 读者数量+1if rw.readerCount.Add(1) < 0 {// 加1以后如果readerCount是负数表示有写者持有了互斥锁// 读者等待信号量释放// 此时读锁已经加上了,等待写者释放信号量就可以了runtime_SemacquireRWMutexR(&rw.readerSem, false, 0)}
}

COPY

读者RTryLock()

这个函数是RWMutex中的TryRLock方法,它试图以非阻塞的方式获取读锁。让我们一步一步地看它是如何工作的。
先看图:

a771641e0c3056616025f39cfa7076b2.png

实现代码


func (rw *RWMutex) TryRLock() bool {for {// 查看当前读者数量c := rw.readerCount.Load()if c < 0 {// 小于0表示有写者已经Penging,加锁失败return false}// 读者数量+1,加读锁成功if rw.readerCount.CompareAndSwap(c, c+1) {return true}}
}

COPY

读者释放读锁RUnlock()

RUnlock方法用于释放读锁。 当一个读者完成读操作并想要释放锁时,就可以调用这个方法。

1af56f016db14721529ee7dd77dfb6bb.png

实现代码


func (rw *RWMutex) RUnlock() {// 释放锁就是-1,// 如果readerCount小于0表示有写者Pending// 进入rUnlockSlowif r := rw.readerCount.Add(-1); r < 0 {rw.rUnlockSlow(r)}
}func (rw *RWMutex) rUnlockSlow(r int32) {// 边界问题处理// r+1 ==0 表示没有读者加锁,却调用了释放读锁// r+1 == -rwmutexMaxReaders表示没有读者加锁,有写者持有互斥锁却释放读锁if r+1 == 0 || r+1 == -rwmutexMaxReaders {race.Enable()fatal("sync: RUnlock of unlocked RWMutex")}// 这表示这是最后一个读者了,最后一个读者要发送信号量通知写者不用等了if rw.readerWait.Add(-1) == 0 {// The last reader unblocks the writer.runtime_Semrelease(&rw.writerSem, false, 1)}
}

COPY

写者加锁Lock()

实现代码


const rwmutexMaxReaders = 1 << 30func (rw *RWMutex) Lock() {// 先持有互斥锁,已经有其他写者持有了互斥锁则等待rw.w.Lock()// rw.readerCount.Add(-rwmutexMaxReaders)这个表示先将readerCount设置为负数表示有写者在等待// 再+rwmutexMaxReaders是为了求出当前reader的数量r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders// 将当前reader的数量加到readerWait表示要等待的读者完成的个数if r != 0 && rw.readerWait.Add(r) != 0 {// 阻塞等待万有的读者完成释放信号量了runtime_SemacquireRWMutex(&rw.writerSem, false, 0)}
}

COPY

写者加锁TryLock()

实现代码


func (rw *RWMutex) TryLock() bool {// 调用互斥锁的TryLock,互斥锁TryLock返回false这儿也直接返回falseif !rw.w.TryLock() {return false}// 加锁成功后// 如果当前还有写者,CompareAndSwap就返回失败if !rw.readerCount.CompareAndSwap(0, -rwmutexMaxReaders) {// 返回失败就释放互斥锁rw.w.Unlock()// 加锁失败return false}// 加锁成功return true
}

COPY

写者解锁Unlock()

实现代码


func (rw *RWMutex) Unlock() {// 这里是对Lock readerCount的逆向操作// 在Lock的时候对readerCount减去了rwmutexMaxReaders,这次加回来;这样就还原了readerCount,即使在Lock之后依然有读者加锁r := rw.readerCount.Add(rwmutexMaxReaders)if r >= rwmutexMaxReaders {race.Enable()fatal("sync: Unlock of unlocked RWMutex")}// 然后循环看当前有多少读者正在等待信号,就释放多少次心血号for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false, 0)}// Allow other writers to proceed.rw.w.Unlock()
}

COPY

测试


package mutex_testimport ("sync""testing""time""github.com/stretchr/testify/assert"
)// 测试读写互斥锁在正常读锁定和解锁情况下的成功执行
func TestRWMutex_ShouldSuccess_WhenNormalReaderLockAndUnLock(t *testing.T) {// 初始化一个读写互斥锁rwmutex := sync.RWMutex{}// 获取读锁rwmutex.RLock()// 设置成功标志为true,使用defer确保在函数结束时释放读锁isSuccess := truedefer rwmutex.RUnlock()// 记录日志表示测试成功t.Log("success")// 断言成功标志为trueassert.True(t, isSuccess)
}// 测试RWMutex的写锁功能是否正常
func TestRWMutex_ShouldSuccess_WhenNormalWriterLockAndUnLock(t *testing.T) {rwmutex := sync.RWMutex{} // 创建一个sync.RWMutex类型的变量rwmutex.Lock()            // 获取写锁isSuccess := true         // 标记为成功状态defer rwmutex.Unlock()    // 确保在函数退出时释放锁,避免死锁t.Log("success")          // 记录测试日志assert.True(t, isSuccess) // 断言isSuccess为true,验证操作成功
}// 函数测试了在正常情况下,
// 读写锁(RWMutex)的读锁(RLock)和写锁(Lock)的加锁与解锁操作是否成功。
func TestRWMutex_ShouldSuccess_WhenNormalReaderWriterLockAndUnLock(t *testing.T) {// 初始化一个读写锁rwmutex := sync.RWMutex{}// 尝试获取读锁并立即释放rwmutex.RLock()rwmutex.RUnlock()// 尝试获取写锁并立即释放rwmutex.Lock()rwmutex.Unlock()// 标记测试为成功isSuccess := true// 记录测试成功日志t.Log("success")// 断言测试结果为真assert.True(t, isSuccess)
}// 测试读写锁在多协程情况下的读写互斥
func TestRWMutex_ShouldSuccess_WhenReaderAndWriterInDifferentRoutine(t *testing.T) {// 初始化一个读写锁和等待组,用于协调不同协程的操作。rwmutex := sync.RWMutex{}wg := sync.WaitGroup{}wg.Add(2) // 预期有两个协程完成操作// 启动一个协程作为读锁持有者go func() {rwmutex.RLock()   // 获取读锁println("reader") // 打印读操作标识rwmutex.RUnlock() // 释放读锁wg.Done()         // 表示读操作完成}()// 启动另一个协程作为写锁持有者go func() {rwmutex.Lock()    // 获取写锁println("writer") // 打印写操作标识rwmutex.Unlock()  // 释放写锁wg.Done()         // 表示写操作完成}()wg.Wait() // 等待所有协程完成操作isSuccess := truet.Log("success")          // 记录测试成功assert.True(t, isSuccess) // 断言测试结果为真
}// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBlockWriter_WhenMultipleReader(t *testing.T) {rwmutex := sync.RWMutex{}ch := make(chan bool)wg := sync.WaitGroup{}wg.Add(2)for i := 0; i < 2; i++ {go func(i int) {wg.Done()rwmutex.RLock()println("reader Locked", i)time.Sleep(10 * time.Second)rwmutex.RUnlock()println("reader UnLocked", i)}(i)}go func() {wg.Wait()println("writer try to accquire wlock")rwmutex.Lock()println("writer has accquired wlock")defer rwmutex.Unlock()ch <- true}()<-chisSuccess := truet.Log("success")assert.True(t, isSuccess)
}// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBlockReaders_WhenWriterIsPresent(t *testing.T) {rwmutex := sync.RWMutex{}wg := sync.WaitGroup{}wg.Add(1)go func() {println("writer try to accquire wlock")rwmutex.Lock()println("writer has accquired wlock")wg.Done()time.Sleep(10 * time.Second)defer rwmutex.Unlock()println("writer has released wlock")}()wg.Wait()wg.Add(2)for i := 0; i < 2; i++ {go func(i int) {println("reader try to lock", i)rwmutex.RLock()println("reader Locked", i)rwmutex.RUnlock()println("reader UnLocked", i)wg.Done()}(i)}wg.Wait()isSuccess := truet.Log("success")assert.True(t, isSuccess)
}// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBlockConcurrentWriters(t *testing.T) {rwmutex := sync.RWMutex{}var blockedWriter boolch := make(chan bool)wg := sync.WaitGroup{}wg.Add(1)go func() {wg.Done()println("Writer 1 try to accquire wlock")rwmutex.Lock()println("Writer 1 has accquired wlock")defer rwmutex.Unlock()time.Sleep(15 * time.Second)}()go func() {wg.Wait()println("Writer 2 try to accquire wlock")rwmutex.Lock()println("Writer 2 has accquired wlock")ch <- truedefer rwmutex.Unlock()}()select {case <-ch:blockedWriter = falsecase <-time.After(20 * time.Second):blockedWriter = true}assert.True(t, blockedWriter)
}// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldLockSuccess_WhenTryingToReadLockTwice(t *testing.T) {rwmutex := sync.RWMutex{}writerWaitGroup := sync.WaitGroup{}writerWaitGroup.Add(1)go func() {rwmutex.RLock()println("readlock locked once")rwmutex.RLock()println("readlock locked twice")rwmutex.RUnlock()rwmutex.RUnlock()defer writerWaitGroup.Done()}()writerWaitGroup.Wait()isSuccess := trueassert.True(t, isSuccess)
}// 测试读写锁在多个写锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenTryingToWriteLockTwice(t *testing.T) {rwmutex := sync.RWMutex{}ch := make(chan bool)go func() {rwmutex.Lock()println("writelock locked once")rwmutex.Lock()println("writelock locked twice")rwmutex.Unlock()rwmutex.Unlock()ch <- true}()isBlocked := falseselect {case <-ch:println("should not execute this block")assert.False(t, isBlocked)case <-time.After(10 * time.Second):isSuccess := trueprintln("executed timeout block")assert.True(t, isSuccess)}}// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenAccquireWriteLockThenReadLock(t *testing.T) {rwmutex := sync.RWMutex{}ch := make(chan bool)go func() {rwmutex.Lock()println("writelock locked once")rwmutex.RLock()println("readlock locked twice")rwmutex.RUnlock()rwmutex.Unlock()ch <- true}()isBlocked := falseselect {case <-ch:println("should not execute this block")assert.False(t, isBlocked)case <-time.After(10 * time.Second):isSuccess := trueprintln("executed timeout block")assert.True(t, isSuccess)}}// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldBeBlocked_WhenAccquireReadLockThenWriteLock(t *testing.T) {rwmutex := sync.RWMutex{}ch := make(chan bool)go func() {rwmutex.RLock()println("readlock locked once")rwmutex.Lock()println("writelock locked twice")rwmutex.Unlock()rwmutex.RUnlock()ch <- true}()isBlocked := falseselect {case <-ch:println("should not execute this block")assert.False(t, isBlocked)case <-time.After(10 * time.Second):isSuccess := trueprintln("executed timeout block")assert.True(t, isSuccess)}}// 测试读写锁在多个读锁情况下的读写互斥
func TestRWMutex_ShouldDeadlockOrBlocked_WhenLockOneGoroutineAccquiredLockAndAnotherGoroutineAccquireLockAgain(t *testing.T) {var rwmutex1, rwmutex2 sync.RWMutexwg := sync.WaitGroup{}wg1 := sync.WaitGroup{}ch := make(chan bool)wg.Add(1)wg1.Add(1)go func() {rwmutex1.Lock()println("rwmutex1 locked")wg.Done()wg1.Wait()println("rwmutex2 try to accquire lock")rwmutex2.Lock()}()go func() {wg.Wait()rwmutex2.Lock()println("rwmutex2 locked")wg1.Done()println("rwmutex1 try to accquire lock")rwmutex1.Lock()ch <- true}()isBlocked := falseselect {case <-ch:println("should not execute this block")assert.False(t, isBlocked)case <-time.After(10 * time.Second):isSuccess := trueprintln("executed timeout block")assert.True(t, isSuccess)}}

参考

 逐步学习Go-sync.RWMutex(读写锁)-深入理解与实战 – 小厂程序员

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/806386.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【uniapp】省市区下拉列表组件

1. 效果图 2. 组件完整代码 <template><view class="custom-area-picker"><view

zabbix企业级监控平台

zabbix部署 安装源 重新创建纯净环境&#xff0c;利用base克隆一台虚拟机server1 给server1做快照&#xff0c;方便下次实验恢复使用 进入zabbix官网https://www.zabbix.com rpm -Uvh https://repo.zabbix.com/zabbix/5.0/rhel/7/x86_64/zabbix-release-5.0-1.el7.noarch.rpm …

D-Link NAS 未授权RCE漏洞复现(CVE-2024-3273)

0x01 免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删…

SVN的介绍

首先SVN是什么&#xff1a; Apache下的一个开源的项目Subversion&#xff0c;通常缩写为 SVN&#xff0c;是一个版本控制系统。 版本控制系统是一个软件&#xff0c;它可以伴随我们软件开发人员一起工作&#xff0c;让我们编写代码的完整的历史保存下来。 目前它的各个版本的…

实现鼠标在页面点击出现焦点及大十字星

近段时间&#xff0c;在完成项目进度情况显示时候&#xff0c;用户在操作鼠标时候&#xff0c;显示当鼠标所在位置对应时间如下图所示 代码实现步骤如下&#xff1a; 1.首先引用 jquery.1.7.js 2.再次引用raphael.js 3.然后引用graphics.js 4.最后引用mfocus.js 其中mfocu…

3. DAX 时间函数-- DATE 日期--一生二,二生三,三生万物

在数据分析过程中&#xff0c;经常需要从一个数据推到另外一个数据&#xff0c;日期数据也是如此&#xff0c;需要从一个日期推到另外一个相关的日期&#xff0c;或者从一群日期推到另外一个相关的日期/一群相关的日期。这一期说的就是日期之间彼此推衍的函数&#xff0c;会比之…

Linux:自动化构建 - make

Linux&#xff1a;自动化构建 - make make基本概念makefile语法变量PHONY make基本概念 make是一个用于自动化编译和构建过程的工具。它主要用于管理大型软件项目的构建过程,帮助开发者更高效地编译和部署代码&#xff0c;并减少人为错误的发生&#xff0c;这使得软件的编译和…

电商技术揭秘十八:电商平台的云计算与大数据应用小结

电商技术揭秘相关系列文章 电商技术揭秘一&#xff1a;电商架构设计与核心技术 电商技术揭秘二&#xff1a;电商平台推荐系统的实现与优化 电商技术揭秘三&#xff1a;电商平台的支付与结算系统 电商技术揭秘四&#xff1a;电商平台的物流管理系统 电商技术揭秘五&#xf…

【STL】list的模拟实现

目录 前言 list概述 list的节点 list的迭代器 list的结构 构造与析构 拷贝构造与赋值 list的元素操作 insert() push_back() push_front() erase() pop_back() pop_front() clear() swap() size() 完整代码链接 前言 如果你对链表还不熟悉或者忘了的话…

Harmony鸿蒙南向驱动开发-PWM

PWM&#xff08;Pulse Width Modulation&#xff09;即脉冲宽度调制&#xff0c;是一种对模拟信号电平进行数字编码并将其转换为脉冲的技术&#xff0c;广泛应用在从测量、通信到功率控制与变换的许多领域中。通常情况下&#xff0c;在使用马达控制、背光亮度调节时会用到PWM模…

微信小程序实现输入appid跳转其他小程序

前言 本文记录wx.navigateToMiniProgram打开另一个小程序API使用方法&#xff0c;并封装为组件。 wxml 部分 输入框用来记录appid&#xff0c;按钮用来查询并跳转。 <view class"container"><input class"input" placeholder"请输入要查…

Flutter Your project requires a newer version of the Kotlin Gradle plugin

在开发Flutter项目的时候,遇到这个问题Flutter Your project requires a newer version of the Kotlin Gradle plugin 解决方案分两步: 1、在android/build.gradle里配置最新版本的kotlin 根据提示的kotlin官方网站搜到了Kotlin的最新版本是1.9.23,如下图所示: 同时在Ko…

python如何写入csv

在使用python对文件操作的过程中&#xff0c;你肯定碰到过对csv文件的操作&#xff0c;下面就python对csv文件的操作进行详述。 CSV&#xff08;Comma-Separated Values&#xff09;逗号分隔符&#xff0c;也就是每条记录中的值与值之间是用分号分隔的。 打开CSV文件并写入一…

实战项目——智慧社区(一)

1、项目介绍 系统功能 登录、修改密码、登出 &#xff08;1&#xff09;首页 &#xff08;1.1&#xff09;数据统计&#xff1a;小区人员统计对比图&#xff0c;占比图 &#xff08;2&#xff09;物业管理 &#xff08;2.1&#xff09;小区管理&#xff1a;小区数据的增删改…

在开发过程中使用 git rebase 还是 git merge

在开发过程中使用 git rebase 还是 git merge Merge(合并)的优点和缺点Rebase(变基)的优点和缺点总结&#xff1a; Git merge 和rebase的目的是一样的&#xff0c;它们都是将多个分支合并成一个。 虽然他们最终的目标是一样的&#xff0c;但这两种方法实现的方式是不同的。那么…

leetcode73 矩阵置零

题目描述 给定一个 m x n 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用原地算法。 输入&#xff1a;matrix [[1,1,1],[1,0,1],[1,1,1]] 输出&#xff1a;[[1,0,1],[0,0,0],[1,0,1]] 输入&#xff1a;matrix [[0,1,2,0],[3,4…

【数据结构】单链表(一)

上一篇【数据结构】顺序表-CSDN博客 我们了解了顺序表&#xff0c;但是呢顺序表涉及到了一些问题&#xff0c;比如&#xff0c;中间/头部的插入/删除&#xff0c;时间复杂度为O(N);增容申请空间、拷贝、释放旧空间会有不小的消耗&#xff1b;增容所浪费的空间... 我们如何去解…

java数据结构与算法刷题-----LeetCode210. 课程表 II

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 深度优先遍历但不进行逆拓扑排序&#xff08;不用栈&#xff09;…

leetcode 343. 整数拆分

题目 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例 1: 输入: n 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输入: n 10 输出: 36 解释: 1…

记一次IP访问MySQL失败多次被自动锁定导致无法连接问题,解决方法只需一条SQL。

&#x1f469;&#x1f3fd;‍&#x1f4bb;个人主页&#xff1a;阿木木AEcru &#x1f525; 系列专栏&#xff1a;《Docker容器化部署系列》 《Java每日面筋》 &#x1f4b9;每一次技术突破&#xff0c;都是对自我能力的挑战和超越。 前言 今天下午还在带着耳机摸鱼&#xff…