Go源码--sync库(2)

news2024/11/17 5:49:17

简介

这边文章主要讲解 Sync.Cond和Sync.Rwmutex

Sync.Cond

简介 sync.Cond 经常用来处理 多个协程等待 一个协程通知 这种场景, 主要 是阻塞在某一协程中 等待被另一个协程唤醒 继续执行 这个协程后续的功能。cond经常被用来协调协程对某一资源的访问 ants协程池就用了这种机制
本文主要讲解Cond 的源码 ,关于其使用已经有许多例子了 本文直接略过
cond 的结构体如下

type Cond struct {
	noCopy noCopy // 告诉编译器 本结构体不能复制

	// L is held while observing or changing the condition
	L Locker            

	notify  notifyList  // 在 Wait 上阻塞的线程 这边只记录 阻塞携程的 指针 真正阻塞是调用的GMP模型 相关功能
	checker copyChecker // 复制检查
}

//使用时 需要 新建一个Cond变量 入参必须有一个锁 用来 锁定 Cond 的 Wait()调用所涉及的资源
func NewCond(l Locker) *Cond {
	return &Cond{L: l}
}

其 主要有三个函数 如下

Wait()

用来阻塞 协程
其源码如下

func (c *Cond) Wait() {
	c.checker.check()                     // 复制检查
	t := runtime_notifyListAdd(&c.notify) // 等待数量+1; 其使用了 汇编的 LOCK 命令来实现加1的原子性操作
	c.L.Unlock()                          // 解锁 ;Wait()代码调用之前必须有 Lock ,可以看到 锁c.L 的作用域直接到了这里
	                                      // 因为 Wait() 一般用在 协程中 且共享内存(也就是变量等)

	runtime_notifyListWait(&c.notify, t)  // 所有协程都阻塞在这里;  大概功能是 首先将本协程指针放入notifyList等待队列
	                                      // 然后进行GMP模型调度 释放当前M 将当前 G指针加入等待队列等待被唤醒 M开始等待可用的G
										  // 所以 g指针存在于两个地方 一个 notifyList 列表 一个 等待队列 这样方便唤醒
	c.L.Lock()                            // 解锁
}

实现wait 效果的 是 runtime_notifyListWait(…)函数 我们来研究下 其代码位置是 runtime/runtime2.go/notifyListWait
源码如下

func notifyListWait(l *notifyList, t uint32) {

	// 上锁 细节可以自行研究
	lockWithRank(&l.lock, lockRankNotifyList)

	// Return right away if this ticket has already been notified.
	if less(t, l.notify) {
		unlock(&l.lock)
		return
	}

	// Enqueue itself.
	// sudog 是某个协程的 阻塞状态信息
	s := acquireSudog()
	// 获取当前协程的指针
	s.g = getg()
	// 可以看做 当前协程在 等待列表中的索引
	s.ticket = t
	s.releasetime = 0
	t0 := int64(0)
	if blockprofilerate > 0 {
		t0 = cputicks()
		s.releasetime = -1
	}
	// 将协程信息 插入 尾部
	if l.tail == nil {
		l.head = s
	} else {
		l.tail.next = s
	}
	l.tail = s
	// 这里涉及到 GMP模型调度 大概功能是 释放当前M 将当前 G加入等待队列等待被唤醒 M开始等待可用的G
	goparkunlock(&l.lock, waitReasonSyncCondWait, traceBlockCondWait, 3)
	if t0 != 0 {
		blockevent(s.releasetime-t0, 2)
	}

	// 释放当前 sudog结构体
	releaseSudog(s)
}

Signal()

唤醒 某一个 调用 Wait()阻塞的协程 继续执行其后续代码
其源码如下

func (c *Cond) Signal() {
	c.checker.check()
	runtime_notifyListNotifyOne(&c.notify)  // link runtime/sema.go/notifyListNotifyOne
											// notifyListNotifyOne 主要使用了GMP调度器的唤醒功能(runtime/proc.go/goready(...)):从notifyList获取某个需要唤醒的g协程指针,
											// 使用GMP调度算法 唤醒这个g协程
											// 将它(g)放入(使用M 操作)运行队列中 必要时唤醒处理器(p)处理这个协程
}

其中runtime_notifyListNotifyOne的唤醒功能使用的是 GMP调度算法 的 ready 调用链 如下
在这里插入图片描述

ready源码如下

func ready(gp *g, traceskip int, next bool) {
	if traceEnabled() {
		traceGoUnpark(gp, traceskip)
	}

	// 获取 当前协程状态
	status := readgstatus(gp)

	// Mark runnable.
	// 获取当前协程 所属的 M
	mp := acquirem() // disable preemption because it can be holding p in a local var
	if status&^_Gscan != _Gwaiting {
		dumpgstatus(gp)
		throw("bad g->status in ready")
	}

	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
	// 将当前协程状态 由等待中 变为可运行
	casgstatus(gp, _Gwaiting, _Grunnable)

	// 通过 M 将协程 G 放入可运行队列
	runqput(mp.p.ptr(), gp, next)
	// 唤醒一个 P
	wakep()
	releasem(mp)
}

可以看到 这段代码 有了我们熟悉的G、M 和P

Broadcast()

唤醒所有 调用 Wait()阻塞的协程 继续执行其后续代码
其源码如下

func (c *Cond) Broadcast() {
	c.checker.check()
	runtime_notifyListNotifyAll(&c.notify) // 主要功能跟 runtime_notifyListNotifyOne 一样 但这个函数 调用的for循环 遍历 整个notifyList列表 唤醒所有协程
}

其底层也就相当 for 循环 执行 了 ready函数

Sync.Rwmutex

读写锁 是为了 解决 mutex的严格互斥的缺点,读读可以并发、读写有条件互斥、写写严格互斥
有争议的就是 读写有条件互斥 这是什么意思呢 我们首先 介绍下 读和写的两种优先模式

  1. 读写时 读优先
    只要有读操作来写操作就阻塞,一直到没有读操作为止,可能会造成写锁饥饿, 这种适合读操作远远大于写操作的场景
  2. 读写时 写优先
    写操作来时 后续的读操作全部阻塞 但是 写操作前的读操作需要完成
    rwmutex 就是采用写优先 所以 有条件互斥 是 写之前的读操作 可以继续完成 写之后的读操作就需要阻塞了

接下来我们来看源码
RWMutex 结构体如下:

type RWMutex struct {
	w           Mutex        // held if there are pending writers                     //  当有写操作挂起时 起作用 控制写操作的时 悲观锁
	writerSem   uint32       // semaphore for writers to wait for completing readers  // 写操作等待完成读操作的信号量
	readerSem   uint32       // semaphore for readers to wait for completing writers  // 读操作等待完成写操作的信号量
	readerCount atomic.Int32 // number of pending readers  // 当前挂起的读操作数,也就是写操作之前和之后发生的 在执行中的总的读操作。readerCount 并发安全 使用 Lock 锁总线
	readerWait  atomic.Int32 // number of departing readers  // 比写操作早到的读操作的数量 执行了 RLock() 但是没执行 RUnlock  
	                                                         // 执行链路  Rlock--->Lock()-->Runlock()
}

其重要的 函数有四个 RLock()、Runlock()、Lock()、Unlock(),其中前两个是控制读操作的,后两个控制写。

  • RLock():根据一定条件来使得读协程阻塞
  • Runlock():根据一定条件来唤醒写协程
  • Lock(): 根据一定条件来阻塞写协程
  • Unlock():根据一定条件来唤醒其后到达的读协程 并释放写锁

我们分别来看下其代码:

RLock()
func (rw *RWMutex) RLock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	if rw.readerCount.Add(1) < 0 { // 小于0 证明前方有写操作(因为写操作会首先readerCount置为小于0) 则后来的读操作阻塞
		// A writer is pending, wait for it.
		runtime_SemacquireRWMutexR(&rw.readerSem, false, 0) // 写之后的读操作阻塞,采用GMP模型 中的功能 将当前G 睡眠 释放 M
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
	}
}
Runlock()
func (rw *RWMutex) RUnlock() { // 执行这个函数 有两种情况
	if race.Enabled {
		_ = rw.w.state
		race.ReleaseMerge(unsafe.Pointer(&rw.writerSem))
		race.Disable()
	}
	if r := rw.readerCount.Add(-1); r < 0 { // 挂起的数量-1 如果挂起的协程数量是负数 说明 有写操作阻塞 则需要检查是否唤起写操作
		// 如果r>=0 :  可能是 Rlock-->Runlock() --->Lock()-->Unlock() 这种情况 readerCount不为负值
		// Outlined slow-path to allow the fast-path to be inlined

		// r<0 开始递减 在写操作之前挂起的读操作数 也就是 readerWait 数 什么情况下会造成这种情况呢 在写操作之前执行 这种情况是 执行完毕 Rlock() 后 写操作 Lock() Runlock()
		// 也就是 Rlock--->Lock()-->Runlock() 这种顺序  这种情况会造成 readerCount为负值
		rw.rUnlockSlow(r)
	}
	if race.Enabled {
		race.Enable()
	}
}

其中 rw.rUnlockSlow( r)的作用是 readerWait数量-1 达到条件后 唤醒写操作 代码如下

func (rw *RWMutex) rUnlockSlow(r int32) {
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		race.Enable()
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	// A writer is pending.
	if rw.readerWait.Add(-1) == 0 { // 写之前的读协程数量-1
		// The last reader unblocks the writer. // 写之前的最后一个 读操作 解锁 写操作
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

Lock()
func (rw *RWMutex) Lock() {
	if race.Enabled {
		_ = rw.w.state
		race.Disable()
	}
	// First, resolve competition with other writers.
	rw.w.Lock() // 将写操作跟其他写操作 进行互斥
	// Announce to readers there is a pending writer.
	r := rw.readerCount.Add(-rwmutexMaxReaders) + rwmutexMaxReaders // 将 readerCount 置为负数 用于阻塞后续 读操作;r是readerCount原始值
	// Wait for active readers.
	if r != 0 && rw.readerWait.Add(r) != 0 { // 将现有 写协程之前的读数量 加入到 等待数量中去 翻译成人话 比写操作早到的 读操作数量 如果不是0 也就是大于0 则写操作需要挂起
		runtime_SemacquireRWMutex(&rw.writerSem, false, 0) // 写协程挂起;GMP 模型 G进入等待队列休眠 M释放  注意:这里是获得写锁的协程睡眠
	}
	if race.Enabled {
		race.Enable()
		race.Acquire(unsafe.Pointer(&rw.readerSem))
		race.Acquire(unsafe.Pointer(&rw.writerSem))
	}
}

其中 const rwmutexMaxReaders = 1 << 30 为什么是这个数 不是 1<<31-1
解答:readerCount 高31位表示是否有写锁,高32表示是否有写锁在等待 则最大就是 1<<30-1 为了使得 其操作后为确定为负数 所以取值 1<<30

Unlock()
func (rw *RWMutex) Unlock() {
	if race.Enabled {
		_ = rw.w.state
		race.Release(unsafe.Pointer(&rw.readerSem))
		race.Disable()
	}

	// Announce to readers there is no active writer.
	r := rw.readerCount.Add(rwmutexMaxReaders) // 将readerCount 还原为原始值 说明没有 写操作了,这时 其值是 写操作后续的读操作
	if r >= rwmutexMaxReaders {
		race.Enable()
		fatal("sync: Unlock of unlocked RWMutex")
	}
	// Unblock blocked readers, if any.
	for i := 0; i < int(r); i++ { // 将读操作挨个唤醒
		runtime_Semrelease(&rw.readerSem, false, 0)
	}
	// Allow other writers to proceed.
	rw.w.Unlock() // 允许其他的写操作继续拥有锁
	if race.Enabled {
		race.Enable()
	}
}

无论是mutex还是rwmutex 其代码量都不大 但是逻辑都比较复杂 需要反复研读大神的代码

下面我们来看下 各种情况的 readCount 和 readWait 的数量 如下图
在这里插入图片描述

大家思考下 引入 rwmutex时能不能仅仅使用RLock()和Runlock()或者Lock()和Unlock()

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

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

相关文章

Android存储空间不足?试试这8个快速解决方案!

在当今的科技时代&#xff0c;Android智能手机已成为我们日常生活的重要组成部分&#xff0c;因为它们保存着我们大量的关键数据。然而&#xff0c;随着我们的使用模式不断扩大&#xff0c;手机内部存储的可用性经常变得有限。手机存储空间不足不仅会损害设备的功能和响应能力&…

数据库系统概论(超详解!!!)第十节 过程化SQL

1.Transact-SQL概述 SQL(Structure Query Language的简称&#xff0c;即结构化查询语言) 是被国际标准化组织(ISO)采纳的标准数据库语言&#xff0c;目前所有关系数据库管理系统都以SQL作为核心&#xff0c;在JAVA、VC、VB、Delphi等程序设计语言中也可使用SQL&#xff0c;它是…

Angular 由一个bug说起之六:字体预加载

浏览器在加载一个页面时&#xff0c;会解析网页中的html和css&#xff0c;并开始加载字体文件。字体文件可以通过css中的font-face规则指定&#xff0c;并使用url()函数指定字体文件的路径。 比如下面这样: css font-face {font-family: MyFont;src: url(path/to/font.woff2…

Docker的安装、启动和配置镜像加速

前言&#xff1a; Docker 分为 CE 和 EE 两大版本。CE 即社区版&#xff08;免费&#xff0c;支持周期 7 个月&#xff09;&#xff0c;EE 即企业版&#xff0c;强调安全&#xff0c;付费使用&#xff0c;支持周期 24 个月。 而企业部署一般都是采用Linux操作系统&#xff0c;而…

电脑找不到mfc140udll怎么修复,总结几种靠谱的修复方法

在日常使用电脑娱乐工作的过程中&#xff0c;我们可能会遇到一些错误提示或程序无法正常运行的情况。其中&#xff0c;一个常见的问题是计算机缺失mfc140u.dll文件。这个问题可能会导致某些应用程序无法启动或运行&#xff0c;给工作和学习带来不便。本文将介绍5种解决计算机缺…

RPC框架原理(一)

RPC框架原理 网络和IO的关系&#xff0c;IO&#xff08;input和output&#xff09;面向的是谁&#xff1f;OSI 7层参考模型&#xff0c;TCP/IP协议为什么会出现一个会话层三次握手socket心跳keep alive四次挥手 网络IO&#xff08;IO模型&#xff09; IO框架底层 学习顺序&…

SpringCloud整合Seata简易使用(注册中心Nacos)

SpringCloud整合Seata解决分布式事务&#xff08;注册中心Nacos&#xff09; Seata下载与配置在Nacos中配置seata相关配置持久化为db时&#xff0c;需要提前在数据库中创建seata数据库&#xff0c;SpringCloud整合Seata服务GlobalTransactional注解使用 本案例是在windows中运行…

抢人!抢人!抢人! IT行业某岗位已经开始抢人了!

所谓抢滩鸿蒙&#xff0c;人才先行。鸿蒙系统火力全开后&#xff0c;抢人已成鸿蒙市场的主题词&#xff01; 智联招聘数据显示&#xff0c;春节后首周&#xff0c;鸿蒙相关职位数同比增长163%&#xff0c;是去年同期的2.6倍&#xff0c;2023年9-12月鸿蒙相关职位数同比增速为3…

长文预警:自动驾驶の核燃料库!Tesla数据标注系统解析

长文预警&#xff1a;自动驾驶の核燃料库&#xff01;Tesla数据标注系统解析 前言 本文整理自原文链接&#xff0c;写的非常好&#xff0c;给了博主很多启发&#xff0c;投原创是因为平台机制&#xff0c;希望能被更多人看到。 掐指一算&#xff0c;又到了该学习的时间&#…

【权威出版/投稿优惠】2024年机器视觉与自动化技术国际会议(MVAT 2024)

2024 International Conference on Machine Vision and Automation Technology 2024年机器视觉与自动化技术国际会议 【会议信息】 会议简称&#xff1a;MVAT 2024截稿时间&#xff1a;(以官网为准&#xff09;大会地点&#xff1a;中国重庆会议官网&#xff1a;www.icmvat.co…

北京崇文门中医医院贾英才与行业共进——第二届海峡两岸中西医结合肾脏病学术大会

第二届海峡两岸中西医结合肾脏病学术大会授牌仪式于2024年6月7号在北京前门国医堂举行。 第二届海峡两岸中西医结合肾脏病学术大会的主要议程可能包括以下内容&#xff1a; 学术讲座&#xff1a;来自海峡两岸的专家学者发表演讲&#xff0c;分享肾脏病防治、透析技术等方面的研…

鸿蒙全栈开发-浅谈鸿蒙~线程模型

前言 如果你现在正巧在找工作&#xff0c;或者琢磨着换个职业跑道&#xff0c;鸿蒙开发绝对值得你考虑一下。 为啥&#xff1f;理由很简单&#xff1a; 市场需求大&#xff1a;鸿蒙生态还在持续扩张&#xff0c;应用开发、系统优化、技术支持等岗位需求旺盛&#xff0c;找工作…

【Text2SQL 论文】C3:使用 ChatGPT 实现 zero-shot Text2SQL

论文&#xff1a;C3: Zero-shot Text-to-SQL with ChatGPT ⭐⭐⭐⭐ arXiv:2307.07306&#xff0c;浙大 Code&#xff1a;C3SQL | GitHub 一、论文速读 使用 ChatGPT 来解决 Text2SQL 任务时&#xff0c;few-shots ICL 的 setting 需要输入大量的 tokens&#xff0c;这有点昂贵…

LabVIEW阀性能试验台测控系统

本项目开发的阀性能试验台测控系统是为满足国家和企业相关标准而设计的&#xff0c;主要用于汽车气压制动系统控制装置和调节装置等产品的综合性能测试。系统采用工控机控制&#xff0c;配置电器控制柜&#xff0c;实现运动控制、开关量控制及传感器信号采集&#xff0c;具备数…

计算机SCI期刊,中科院2区,IF=6.9,收稿范围非常广泛

一、期刊名称 Journal of King Saud University—Computer and Information Sciences 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;6.9 中科院分区&#xff1a;2区 三、期刊征稿范围 《沙特国王大学计算机与信息科学杂…

如何让tracert命令的显示信息显示*星号

tracert命令如果在中间某一个节点超时&#xff0c;只会在显示信息中标识此节点信息超时“ * * * ”&#xff0c;不影响整个tracert命令操作。 如上图所示&#xff0c;在DeviceA上执行tracert 10.1.2.2命令&#xff0c;缺省情况下&#xff0c;DeviceA上的显示信息为&#xff1a;…

吊车报警的工作原理和使用场景_鼎跃安全

在现代建筑施工过程中&#xff0c;经常使用大型机械设备&#xff0c;如挖掘机、吊车、打桩机等&#xff0c;这些设备在施工过程中发挥着越来越重要的作用&#xff1b;同时&#xff0c;这些设备的作业频繁进行作业&#xff0c;对于接触到高压电线的风险也随之增加。大型机械设备…

【Text2SQL 论文】MAC-SQL:多个 Agents 合作来解决 Text2SQL

论文&#xff1a;MAC-SQL: A Multi-Agent Collaborative Framework for Text-to-SQL ⭐⭐⭐⭐ arXiv:2312.11242, 北航 & Tencent Code: MAC-SQL | GitHub 文章目录 一、论文速读二、MAC-SQL2.1 Selector agent2.2 Decomposer agent2.3 Refiner agent 三、指令微调的 SQL-L…

企业费用标准如何制定?

在当前宏观经济环境和市场竞争日益激烈的背景下&#xff0c;国内很多企业的费用管理流程依旧面临诸多挑战。特别是制造业、零售业等人员众多的企业&#xff0c;如何通过制定精细化、自动化的企业费用标准来实现降本增效&#xff0c;已经成为企业财务流程优化的首要目标。 企业…

【Microelectronic Systems】期末速通

PART1 嵌入式系统概述与玩转mbed 1 嵌入式系统&#xff0c;微控制器&#xff0c;与ARM 1.1什么是嵌入式系统&#xff1f; 微处理器不仅仅存在于通用计算机中&#xff0c;也可以安置在一些不需要计算的设备内部&#xff0c;比如洗衣机&#xff0c;摄像机。微处理器常常可以控制…