Go-知识并发控制RWMutex

Go-知识并发控制RWMutex

  • 1. 介绍
  • 2. 原理
    • 2.1 读写锁的数据结构
    • 2.2 接口定义
    • 2.3 Lock() 写锁定 原理
    • 2.4 Unlock() 写锁定解锁 原理
    • 2.5 RLock() 读锁定 原理
    • 2.6 RUnlock() 读锁定解锁 原理
  • 3. 场景分析
    • 3.1 写锁定如何阻塞写锁定
    • 3.2 写锁定如何阻塞读锁定
    • 3.3 读锁定如何阻塞写锁定
    • 3.4 写锁定不会被 饿死

gitio: https://a18792721831.github.io/

1. 介绍

互斥锁 Mutex 是串行加锁,拿到锁之后,不管是读操作还是写操作,对于 Mutex 来说,是等价的。
但是在并发里面,如果仅仅是读操作,不改变数据的前提下,是可以共享的,多个协程读取到的数据都是可信的。
Mutex 存在这几个问题:

  • 写锁需要阻塞写锁: 一个协程拥有写锁时,其他协程写操作需要阻塞
  • 写锁需要阻塞读锁: 一个协程拥有写锁时,其他协程读操作需要阻塞
  • 读锁需要阻塞写锁: 一个协程拥有读锁时,其他协程写操作需要阻塞
  • 读锁不能阻塞读锁: 一个协程拥有读锁时,其他协程读操作不需要阻塞

而 RWMutex 是 Mutex 的一个增强版,解决了上面的4个问题

2. 原理

2.1 读写锁的数据结构

在源码包src/sync/rwmutex.go中定义了读写锁的数据结构

type RWMutex struct {
	w           Mutex  // 用于控制多个写锁,获得写锁需要先获取该锁
	writerSem   uint32 // 写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
	readerSem   uint32 // 读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
	readerCount int32  // 记录读者个数
	readerWait  int32  // 记录写阻塞时读者个数
}

读写锁内部使用 Mutex 互斥锁,用于将多个写操作隔离开来。

2.2 接口定义

RWMutex 提供了这几个接口用于操作锁:

  • RLock(): 读锁定
  • RUnlock(): 读锁定解锁
  • Lock(): 写锁定
  • Unlock(): 解除写锁定
  • TryLock(): 以非阻塞方式尝试写锁定( >= Go 1.18)
  • TryRLock(): 以非阻塞方式尝试读锁定( >= Go 1.18)

2.3 Lock() 写锁定 原理

写锁定需要实现两个动作: 1. 获取互斥锁;2. 阻塞等待所有读操作结束(如果存在未结束的读操作)

// 锁定rw进行写入。
// 如果锁已经被锁定用于读取或写入,
// 锁定块,直到锁定可用。
func (rw *RWMutex) Lock() {
	// 首先,解决与其他作家的竞争。
	// rw.w 是 Mutex 锁,Lock 操作 会阻塞获取锁
	// 如果有多个写操作竞争锁,那么会使用 Mutex 的策略,进行排队或自旋 获取锁
	rw.w.Lock()
	// 向读者宣布有一个待定的作者。
	// 假设 raderCount = 2 , rwmutexMaxReaders = 100
	// 并发安全的 2 - 100 = -98 
	// 并发不安全的 -98 + 100 = 2 
	// 拿到了 raderCount
	// 因为 raderCount 是 int32 类型,并不是 atomic.Int32,所以不能使用 Load() 方法并发安全的获取值
	// func AddInt32(addr *int32, delta int32) (new int32) 
	// 直接调用底层方法,让一个普通的 int32 拥有了 atomic 的能力
	r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
	//等待活动的读卡器。
	// 如果读操作不为空,那么将当前写操作记录到 写阻塞时,读操作的个数
	if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
	    // 等待读操作结束后释放信号量,这里是阻塞的,等待 writerSem 地址的信号量
		runtime_SemacquireMutex(&rw.writerSem, false, 0)
	}
}

需要注意一点,在 Lock() 操作里面 readerCount 因为溢出,现在是负值

最开始的时候, readerCount 是 0 , 当 Lock 后,是 负的 max
在这里插入图片描述

2.4 Unlock() 写锁定解锁 原理

写锁定解锁要实现的两个动作: 1. 唤醒因读锁定而被阻塞的协程(如果有)(写操作的同时发生的读操作,因为写操作还未完成,所以读操作需要等待写操作完成后才能进行读); 2. 解除互斥锁(写操作必须先获取 Mutex )

//解锁解锁rw进行写入。如果rw为
//未锁定,无法在输入解锁时写入。
//
//与Mutex一样,锁定的RWMutex与特定的
//goroutine。一个goroutine可以RLock(Lock)一个RWMutex,然后
//安排另一个goroutine来解锁它。
func (rw *RWMutex) Unlock() {
	//向读者宣布没有活动的写入程序。
	// 因为在 Lock 中, raderCount 的值被置为负值
	// 所以在这里在 加上 rwmutexMaxReaders ,readerCount 的值就变成了正值
	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
	// 如果出现溢出,,那么是错误的
	if r >= rwmutexMaxReaders {
		throw("sync: Unlock of unlocked RWMutex")
	}
	// 取消阻止被阻止的读卡器(如果有的话)。
	// 因为存在 r 个读操作因为写操作而阻塞,当写操作完成时,就需要唤醒读操作
	for i := 0; i < int(r); i++ {
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	//允许其他写入程序继续。
	// 释放锁,等待下次写操作
	rw.w.Unlock()
}

这里其实有一点小的不同,当写操作锁释放时,如果即存在读操作等待,又存在写操作等待,那么是优先读操作的。
也就是: 读 -> 写 -> 读 -> …

这里引申出来一个思考,假设在读操作中,不断的派生,导致读操作占用一直无法结束,那么就会导致写操作加锁也无法加锁
写锁定解除的时候,将 readerCount + max ,获取到真正的 reader waiter
然后释放 reader waiter 个信号量,唤醒全部等待的读锁定
在这里插入图片描述

2.5 RLock() 读锁定 原理

读操作加锁需要的动作: 1. 增加读操作计数器; 2. 阻塞等待写锁定解锁(如果有)

// 在关系通过以下方式指示给竞赛检测器之前发生:
// - Unlock  -> Lock:  readerSem
// - Unlock  -> RLock: readerSem
// - RUnlock -> Lock:  writerSem
//
// 以下方法暂时禁用竞赛同步的处理
// 事件,以便为比赛提供更精确的模型 探测器
//
// 例如,原子。RLock中的AddInt32不应显示为提供
// 获取发布语义,这将错误地同步比赛
// 读者,从而可能错过种族。
// RLock锁定rw进行读取。
//
// 它不应用于递归读取锁定;被锁住的锁
// 调用将阻止新读者获取锁。请参阅
// RWMutex类型的文档。
func (rw *RWMutex) RLock() {
    // 在写锁定里面,会将 raderCount 置为负数,所以如果给 raderCount + 1 依然小于 0 
    // 那么说明至少有一个读锁定在阻塞等待写锁定解锁
    // 换句话说,当 raderCount + 1 小于 0 ,说明有协程正在 写锁定
	if atomic.AddInt32(&rw.readerCount, 1) < 0 {
		//有一个写入程序正在挂起,请稍候。
		runtime_SemacquireMutex(&rw.readerSem, false, 0)
	}
}

因为在 写锁定 操作中,是直接减去最大值,所以这里实际上是不会限制读锁加锁数量的,也就是说,如果不断派生,那么读操作可能永远无法结束。
写锁定饿死?
这里要分为两种场景:

  1. RWMutex 无锁定,那么 readerCount > 0 ,就是读锁定个数
  2. RWMutex 有写锁定,那么 readerCount < 0 , 读锁定个数 = readerCount + max
    在这里插入图片描述

2.6 RUnlock() 读锁定解锁 原理

读锁定解锁的动作: 1. 减少读操作计数器; 2. 唤醒等待的写锁定(如果有)

// RUnlock撤消单个RLock调用;
// 它不会影响其他同时阅读的人。
// 如果rw未锁定以进行读取,则是运行时错误
// 在进入RUnlock时。
func (rw *RWMutex) RUnlock() {
    // 在 RUnlock 中,readerCount 是不能变为负值的,如果变为负值,
    // 说明读锁在未拿到锁的情况下,调用了 RUnlock
    // 也有可能是还未加锁就解锁,加锁 readerCount++, 解锁 readerCount--
    // 还有一种情况是,存在读锁定的时候,出现了写锁定,写锁定会将 readerCount 变为负值
	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
		//勾勒出慢速路径以允许快速路径内联
		rw.rUnlockSlow(r)
	}
}
func (rw *RWMutex) rUnlockSlow(r int32) {
    // 如果 r - 1 = -1 那么说明是未加锁就解锁,异常
    // 如果 r + 1 = - max , 加锁数量超过最大值,异常
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		throw("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
	// 如果 是 写锁定情况下 读锁定解锁,此时会判断 readerWait 的值
	// 防止 写锁定 饿死,在写锁定的时候,会同步 readerWait = readerCount
	// 比如 写锁定时 readerCount = n , 此时 readerWait = n ,同时会将 readerCount 变为负值
	// 变为负值后,读锁定就会阻塞
	// 然后等待 读锁定解锁
	// 在读锁定解锁的时候,进入 rUnlockSlow 就说明,有 写锁定 在阻塞
	// 此时 readerWait > 0 , 那么每次读锁定解锁,会将 readerWait--
	// 当 readerWait 等于 0  的时候,需要唤醒写锁定
	// 用这样的逻辑防止 写锁定 饿死
	// 只有最后一个才唤醒
	if atomic.AddInt32(&rw.readerWait, -1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

在这里插入图片描述

3. 场景分析

3.1 写锁定如何阻塞写锁定

RWMutex 包含了一个 Mutex ,获取写锁定必须获取 Mutex ,如果已经有写锁定获取了 Mutex
那么后续的写锁定就必须等待前面的写锁定释放 Mutex 才能继续写锁定。

3.2 写锁定如何阻塞读锁定

写锁定会将 readerCount 的值变为负值,在读锁定里面,如果 readerCount + 1 < 0 那么会进入阻塞。
将 readerCount 的值变为负值,不会导致 readerCount 的原始值丢失,只需要 readerCount + max
就能重新拿到真实的值,并且期间针对 readerCount 的运算结果也能继续保留下来。

这个实现非常精妙,值得学习。

3.3 读锁定如何阻塞写锁定

读锁定会将 readerCount + 1 ,写锁定读取到 readerCount 不为 0 ,就会阻塞,
同时设置 readerWait = readerCount ,防止写锁定饿死

3.4 写锁定不会被 饿死

写锁定需要等待读锁定解锁后才能获取锁,写锁定的等待期间可能还有新的读锁定,那么可能永远等不到全部的读锁定解锁。
这样写锁定就出现 饿死
但是实际上是写锁定会将 readerCount 的值放到 readerWait 中,并且将 readerCount 的值变为负值
当 readerCount 的值变为负值,就不会有新的 读锁定了,读锁定因为 readerCount + 1 < 0 而进入阻塞
在 读锁定解锁的时候,如果 readerWait + 1 = 0 ,就会唤醒写锁定
相当于 当读锁定还未解锁时,写锁定会将当前读锁定的数量记下来,然后阻塞新增新的读锁定
等待记录的读锁定解锁后,就进行写锁定
强行将读锁定分割,避免饿死

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT 5.14.2 应用程序打包

我们可以直接通过开发工具预览我们的程序。但是当要把开发好的程序给别人使用的时候&#xff0c;我们就需要把程序打包成可执行的exe&#xff0c;然后把这个exe文件和其他相关的文件一起发给别人&#xff0c;这样别人就可以使用了。 一、生成可独立运行的exe (一)、编译程序的…

调教LLaMA类模型没那么难,LoRA将模型微调缩减到几小时

简介&#xff1a; 调教LLaMA类模型没那么难&#xff0c;LoRA将模型微调缩减到几小时 LoRA 微调方法&#xff0c;随着大模型的出现而走红。 最近几个月&#xff0c;ChatGPT 等一系列大语言模型&#xff08;LLM&#xff09;相继出现&#xff0c;随之而来的是算力紧缺日益严重。虽…

移动端消息中心,你未必会设计,发一些示例出来看看。

APP消息中心是一个用于管理和展示用户收到的各种消息和通知的功能模块。它在APP中的作用是提供一个集中管理和查看消息的界面&#xff0c;让用户能够方便地查看和处理各种消息。 以下是设计APP消息中心的一些建议&#xff1a; 1. 消息分类&#xff1a; 将消息按照不同的类型进…

【网络编程】多进程服务器端

并发服务器的实现 多进程服务器:通过创建多个进程提供服务多路复用服务器:通过捆绑并统一管理IO对象提供服务。多线程服务器:通过生成与客户端等量的线程提供服务。、 理解进程process 定义&#xff1a;占用内存空间的正在运行的程序。 CPU核和进程数&#xff1a;1个CPU 中…

电机控制安全:PWM 直通

在 H 桥中使用互补 PWM 时的一个主要考虑因素是短路的可能性&#xff0c;也称为“击穿”。 如图 5 所示&#xff0c;如果同一支路上的两个开关同时打开&#xff0c;H 桥配置可能会导致电源和接地之间发生直接短路。 如果同一条腿上的两个开关同时打开&#xff0c;则可能会发生…

ConcurrentHashMap如何保证线程安全?

ConcurrentHashMap 是 HashMap 的多线程版本&#xff0c;HashMap 在并发操作时会有各种问题&#xff0c;比如死循环问题、数据覆盖等问题。而这些问题&#xff0c;只要使用 ConcurrentHashMap 就可以完美解决了&#xff0c;那问题来了&#xff0c;ConcurrentHashMap 是如何保证…

EvaluLLM: LLM Assisted Evaluation of Generative Outputs论文阅读

Abstract 随着大型语言模型&#xff08;LLM&#xff09;能力的迅速提升&#xff0c;衡量自然语言生成&#xff08;NLG&#xff09;系统输出质量变得越来越困难。传统的指标如BLEU和ROUGE依赖于参考数据&#xff0c;通常不适用于需要创造性或多样化输出的任务。人工评估是一种选…

救命!接手了一个老项目,见到了从业10年以来最烂的代码!

后台回复“书籍”&#xff0c;免费领取《程序员书籍资料一份》 后台回复“5000”&#xff0c;免费领取面试技术学习资料一份 在程序员这个行业从业快10年了&#xff0c;每过几个月回头看看自己写的代码&#xff0c;都会觉得写的也太烂了&#xff0c;不敢想象是自己之前写的。…

官网首屏:太漂亮了,真是着了它的魔,上了它的道。

大气的企业官网致力于提供用户友好的界面和优质的用户体验。网页经过精心设计和开发&#xff0c;旨在展示客户的品牌形象和产品信息&#xff0c;并为用户提供便捷的服务和沟通渠道。 官网设计追求简洁、美观、易用的原则&#xff0c;以吸引用户的注意力并提供清晰的导航和信息…

【文档智能 RAG】RAG增强之路-智能文档解析关键技术难点及PDF解析工具PDFlux

前言 在私域知识问答和企业知识工程领域&#xff0c;结合Retrieval-Augmented Generation&#xff08;RAG&#xff09;模型和大型语言模型&#xff08;LLM&#xff09;已成为主流方法。然而&#xff0c;企业中存在着大量的PDF文件&#xff0c;PDF解析的低准确性显著影响了基于…

git配置3 - 一个git仓库同时push到多个代码托管平台

1. 应用场景2. 单个代码托管平台时3. 多个代码托管平台时 3.1. 在github上创建一个项目3.2. 添加远端仓库关联3.3. 查看关联的远端仓库3.4. 推送代码到github 1. 应用场景 场景一&#xff1a; 你有一个开源的项目&#xff0c;你希望托管到多个开源代码托管平台。比如github…

springer 在线投稿编译踩坑

springer投稿&#xff0c;在线编译踩坑总结 注意&#xff1a; 有的期刊需要双栏&#xff0c;而预定义的模板中可能为单栏&#xff0c;需要增加iicol选项。 例如&#xff1a; \documentclass[sn-mathphys-num]{sn-jnl}% —>\documentclass[sn-mathphys-num, iicol]{sn-jnl}…

关于BERT和embedding

embedding到一个低维向量&#xff0c;但是需要回到onehot高维表示&#xff0c;所以大部分填词游戏最后都需要加上一个MLP接头。 word2vec如此简单的结构&#xff0c;学习到的是embedding 基于计数的统计方法和word2vec融合就形成了glove词嵌入模型 总结&#xff1a;通过各种…

新版嘎嘎快充互联互通系统配置文档

宝塔环境配置 登录宝塔账号&#xff0c;安装nginx、mysql5.7、php7.2、supervisor、redisphp安装扩展&#xff1a; 1&#xff09;安装swooleloader72 将嘎嘎官方提供的swoole_loader_72_nts.so文件上传到 /www/server/php/72/lib/php/extensions/no-debug-non-zts-20170718…

openGauss学习笔记-300 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQL Rewriter SQL语句改写

文章目录 openGauss学习笔记-300 openGauss AI特性-AI4DB数据库自治运维-DBMind的AI子功能-SQL Rewriter SQL语句改写300.1 概述300.2 使用指导300.2.1 前提条件300.2.2 使用方法示例300.3 获取帮助300.4 命令参考300.5 常见问题处理openGauss学习笔记-300 openGauss AI特性-AI…

数智教育创新如何向未来?腾讯云与你探索革新之路

引言 随着科技革命的快速发展&#xff0c;掀起教育领域的变革&#xff0c;新理念、新技术、新模式、新应用正不断涌现&#xff0c;正塑造着教育的未来形态。未来科技还将如何赋能教育创新&#xff1f; 5月31日&#xff0c;由腾讯云TVP 与西安电子科技大学联合举办的「数智教育的…

618洗地机全网热门推荐,跟着买错不了

步入酷热夏天&#xff0c;家中的清洁工作也迎来了新的挑战。天气炎热&#xff0c;细菌、异味滋生的困扰让日常打扫变得不再轻松&#xff0c;这时一台高性能的洗地机就成了提升生活品质的必备良品。不同于洗地机的技术与类别繁多&#xff0c;洗地机虽原理不复杂&#xff0c;但在…

JProfiler 性能分析案列——dump.hprof 堆内存快照文件分析排查内存溢出

在 windows 环境下实现。 一、配置 JVM 参数 配置两个 JVM 参数&#xff1a; -XX:HeapDumpOnOutOfMemoryError&#xff0c;配置这个参数&#xff0c;会在发生内存溢出时 dump 生成内存快照文件&#xff08;xxx.hprof&#xff09;-XX:HeapDumpPathF:\logs&#xff0c;指定生成…

04.VisionMaster 机器视觉找圆工具

VisionMaster 机器视觉找圆工具 定义 先检测出多个边缘点然后拟合成圆形&#xff0c;可用于圆的定位与测量 注意&#xff1a;找圆工具 最好和【位置修正】模块一起使用。具体可以看下面的示例。 参数说明&#xff1a; 扇环半径&#xff1a;圆环ROI的内外圆半径 边缘类型&a…

C51学习归纳13 --- AD/DA转换

AD/DA转换实现了计算机和模拟信号的连接&#xff0c;扩展了计算机的应用场景&#xff0c;为模拟信号数字化提供了底层支持。 AD转换通常是多个输入通道&#xff0c;使用多路选择器连接到AD开关&#xff0c;实现AD多路复用的目的&#xff0c;提高利用率。 AD/DA转换可以使用串口…