Go 源码之互斥锁 Mutex

news/2024/6/24 7:40:52/文章来源:https://blog.csdn.net/weixin_37755580/article/details/137236413

文章目录

  • 一、总结
  • 二、源码
    • (一)Mutex
    • (二) Lock
    • (三)Unlock
  • 三、常见问题
          • 有劳各位看官 `点赞、关注➕收藏 `,你们的支持是我最大的动力!!!
          • 接下来会不断更新 `golang` 的一些`底层源码及个人开发经验`(个人见解)!!!
          • 同时也欢迎大家在评论区提问、分享您的经验和见解!!!

一、总结

  • 锁不可复制:拷贝互斥锁同时会拷贝锁的状态,容易造成死锁

  • 不是可重入锁,并且一个协程上锁,可以由另外一个协程解锁

  • mutex 锁结构

    • state:32位,锁状态,bitmap 设计,
      • 1 mutexLocked :低1位 锁定状态
      • 2 mutexWoken :低2位,从正常模式被唤醒
      • 3 mutexStarving 是低3位,进入饥饿模式
        1. mutexWaiterShift 剩下 29 位,当前互斥锁上等待者的数量
    • sema:协程等待信号量,用于控制goroutine的阻塞与唤醒
  • 上锁
    image.png

  • 解锁
    image.png

  • 锁的两种模式

  • 正常模式
    在正常模式下等待的 g 按照先进先出的方式获取锁
    新 g 会 自旋 ,并且和刚唤醒的 g 竞争锁,新 g 会优先获得锁,会导致刚被唤起的 g 一直获取不到锁,
    这种情况的出现会导致线程长时间被阻塞下去,所以Go语言在1.9中进行了优化,引入了 饥饿模式
  • 饥饿模式
    为了解决等待 goroutine 队列的长尾问题(饿死)
    当 g 超过 1ms 没有获取到锁,就会将当前互斥锁切换到饥饿模式
    等待的 g 按照先进先出的方式获取锁
    饥饿模式下,新进来的 G 不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部。
    在这种情况下,这个被唤醒的 goroutine 会优先加入到等待队列的前面,防止饿死
    如果一个 goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式

二、源码

(一)Mutex

const (mutexLocked = 1 << iota 									// 1 mutex 锁定状态mutexWoken 													// 2 mutex 从正常模式被唤醒mutexStarving 												// 4 mutex进入饥饿状态mutexWaiterShift = iota 									// 3 当前互斥锁上等待者的数量
)type Mutex struct { 												// Mutex 不可被复制state int32 // 32位,锁状态,bitmap 设计,低三位表示锁的状态,剩下 29 位表当前互斥锁上等待者的数量sema  uint32 // 缓冲信号量,用来控制等待goroutine的阻塞休眠和唤醒,可以理解为一个队列
}

image.png

(二) Lock

  • 直接 CAS 进行原子操作上锁,成功则返回,失败则执行 lockSlow()
  • 上锁失败,执行 lockSlow(),内部持续 for 循环
    • 支持自旋(正常模式、cpu空闲、自旋次数<4),则进入自旋
    • 不支持自旋:两种模式
      • 正常模式:加入尾部队列,按照先进先出的方式加入队列等待获取锁
      • 饥饿模式:当goroutine超过1ms没有获取到锁,就会将当前互斥锁切换到饥饿模式,如果当前goroutine 存在队列中,则移动到队头,然后按照先进先出的方式获取锁,防止饿死

func (m *Mutex) Lock() {if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 	// 直接 CAS 修改锁的状态,将 state=0 改为 1 return}m.lockSlow()															// CAS 无法直接上锁,则执行慢路径
}
func (m *Mutex) lockSlow() {var waitStartTime int64 											// 用来计算waiter的等待时间starving := false 													// 是否是饥饿模式awoke := false 														// 是否唤醒iter := 0 															// 自旋次数old := m.state 														// 旧的锁状态for {// 支持自旋:锁不是饥饿模式 && cpu 支持继续自旋(<=4次)if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// g不是唤醒状态 && // 没有其他正在唤醒的goroutine && // 等待队列中有正在等待的goroutine// && 尝试将当前锁的低2位的Woken状态位设置为1,表示已被唤醒, 这是为了通知在解锁Unlock()中不再唤醒其他waiterif !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true                                     // 设置当前goroutine唤醒成功}runtime_doSpin()                                    // 进行自旋iter++                                              // 自旋次数++old = m.state                                       //更新锁状态continue}new := oldif old&mutexStarving == 0 { new |= mutexLocked                          // 非饥饿模式下进行加锁}if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift                // 等待着数量+1}if starving && old&mutexLocked != 0 {new |= mutexStarving	                    // 加锁的情况下切换为饥饿模式}if awoke {                                     // goroutine 唤醒的时候进行重置标志if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}if atomic.CompareAndSwapInt32(&m.state, old, new) {     //设置新的状态if old&(mutexLocked|mutexStarving) == 0 {break }queueLifo := waitStartTime != 0if waitStartTime == 0 {                       // 判断是不是第一次加入队列waitStartTime = runtime_nanotime()         // 如果之前就在队列里面等待了,加入到对头}        runtime_SemacquireMutex(&m.sema, queueLifo, 1) // 阻塞等待// 检查锁是否处于饥饿状态starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// 如果锁处于饥饿状态,直接抢到锁if old&mutexStarving != 0 {if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}//设置标志,进行加锁并且waiter-1delta := int32(mutexLocked - 1<<mutexWaiterShift)//如果是最后一个的话清除饥饿标志if !starving || old>>mutexWaiterShift == 1 {//退出饥饿模式				delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}
}

(三)Unlock

  • 直接 CAS 进行原子操作解锁,成功则返回,失败则执行 unlockSlow()
  • 解锁失败,执行 unlockSlow()
    • 正常模式
      • 如果当前队列中没有waiter,只有自己本身,直接解锁返回
      • 如果当前队列中有waiter,解锁后唤醒下个等待者 runtime_Semrelease(&m.sema, false, 1)
    • 饥饿模式
      • 饥饿模式直接将锁的控制权交给队列中队头等待的waiter
func (m *Mutex) Unlock() {new := atomic.AddInt32(&m.state, -mutexLocked)  // 直接 CAS 修改锁的状态 if new != 0 {// 不等于0说明解锁失败,m.unlockSlow(new)}
}
func (m *Mutex) unlockSlow(new int32) {//解锁一个未加锁的Mutex会报错if (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {// 正常模式下,没有waiter或者在处理事情的情况下直接返回if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}//如果有等待者,设置mutexWoken标志,waiter-1,更新statenew = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// 饥饿模式下会直接将mutex交给下一个等待的waiter,让出时间片,以便waiter执行runtime_Semrelease(&m.sema, true, 1)}
}

三、常见问题

1. sema 字段的含义作用
在正常模式下,一个goroutine先通过自旋方式获得锁,如果还不能获取锁,则通过信号量进行排队等

(所有等待者都会按照先入先出的顺序排队)但是当被唤醒后,第一个等待者并不会立即获得锁,而是需要和那些正在处于自旋阶段,尚未加入到队列中的routine竞争,如果抢不到锁的话,重新插入到队列的头部,而当这个goroutine加锁等待的时间超过了1ms之后,它会把mutex由正常模式切换到饥饿模式,这种模式下锁的所有权直接传递给头部的routine。后来者不会自旋,也不会尝试获取锁,直接加到队列尾部

2.什么是CAS,什么是原子操作
CAS(Compare and Swap)比较并交换,比较两个值,如果他们两者相等就把他们交换。这是一个由CPU硬件提供并实现的原子操作。
原子操作:操作系统提高的锁机制来保证操作的原子性和线程安全性。这种锁机制可以使执行原子操作的 CPU 独占内存总线或者缓存,并防止其他 CPU 对同一内存地址进行读写操作,从而避免了数据竞争的问题
具体来说,在执行原子操作时,CPU 会向内存总线或者缓存发送锁请求信号,然后等待锁授权。一旦锁授权成功,CPU 就可以将操作的结果写入内存,然后释放锁。其他 CPU 在锁被释放之前不能对同一内存地址进行读写操作,从而保证了操作的原子性和线程安全性。
需要注意的是,原子操作增加 CPU 的开销和内存带宽的消耗

3. 锁的正常模式和饥饿模式?
image.png

4.为什么锁不可复制
因为互斥锁没有绑定 gid,复制锁会复制锁的状态,容易出现死锁

5.什么情况下mutex会从饥饿模式变成正常模式呢?
如果当前 goroutine 等待锁的时间超过了 1ms,互斥锁就会切换到饥饿模式。
如果当前 goroutine 是互斥锁最后一个waiter,或者等待的时间小于 1ms,互斥锁切换回正常模式。

5. goroutine能进入自旋的条件

  • 当前互斥锁处于正常模式,不处于饥饿模式
  • 积累的自旋次数小于最大自旋次数(active_spin=4
  • cpu 核数大于 1
  • 有空闲的 P
  • 当前 goroutine 所挂载的 P 下,本地待运行队列为空
//go:linkname sync_runtime_canSpin sync.runtime_canSpin
func sync_runtime_canSpin(i int) bool {// active_spin = 4if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {return false}if p := getg().m.p.ptr(); !runqempty(p) {return false}return true
}
有劳各位看官 点赞、关注➕收藏 ,你们的支持是我最大的动力!!!
接下来会不断更新 golang 的一些底层源码及个人开发经验(个人见解)!!!
同时也欢迎大家在评论区提问、分享您的经验和见解!!!

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

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

相关文章

将 EDI 从 VAN 迁移到知行之桥:EDI 成本降低 90%

G公司帮助零售商在网上和实体店取得成功&#xff0c;是来自300家顶级制造商网络中包含狩猎&#xff0c;钓鱼&#xff0c;露营和体育用品等45,000 多种商品的批发分销商。 通过可靠的客户服务、良好的定价和超越标准大卖场产品的库存量&#xff0c;G公司的重点是为零售商、电子…

ZooKeeper 的持久化机制

持久化的定义&#xff1a; 数据&#xff0c;存到磁盘或者文件当中。机器重启后&#xff0c;数据不会丢失。内存 -> 磁盘的映射&#xff0c;和序列化有些像。 ZooKeeper 的持久化&#xff1a; SnapShot 快照&#xff0c;记录内存中的全量数据TxnLog 增量事务日志&#xff…

网络播放解码器 SIP-7102网络音频解码播放器

网络播放解码器 SIP-7102网络音频解码播放器 SIP-7102是一款支持SIP协议的网络音频解码播放器&#xff0c;具有10/100M以太网接口&#xff0c;其接收网络的音频数据&#xff0c;通过设备的DSP及放大电路&#xff0c;可以直接连接两路15W的扬声器。可以用于公共广播、报警系统、…

将 Three 带到 Vue 生态系统,TresJs 中文文档上线

将 Three 带到 Vue 生态系统&#xff0c;TresJs 中文文档上线 中文文档上线入门指南 ThreeJS 在创建 WebGL 3D 网站方面是一个奇妙的库&#xff0c;同时他也是一个保持不断更新的库&#xff0c;一些对其封装的维护者&#xff0c;如 TroisJS&#xff0c;往往很难跟上其所有的更…

主流好用的 Markdown 编辑器介绍

在当今程序员的日常工作中&#xff0c;Markdown 已经成为了一种常用的文本标记语言&#xff0c;它简洁、易读、易写&#xff0c;被广泛应用于写作、文档编写、博客撰写等场景。为了更高效地编辑和管理 Markdown 格式的文档&#xff0c;选择一款功能强大、易用的 Markdown 编辑器…

Photoshop 2024 中文---专业图像处理软件的又一次飞跃

Photoshop 2024是一款功能强大的图像处理软件&#xff0c;广泛应用于创意设计和图像处理领域。它提供了丰富的绘画和编辑工具&#xff0c;包括画笔、铅笔、颜色替换、混合器画笔等&#xff0c;使用户能够轻松进行图片编辑、合成、校色、抠图等操作&#xff0c;实现各种视觉效果…

基于单片机控制的智能轿车停车场设计

**单片机设计介绍&#xff0c;基于单片机控制的智能轿车停车场设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机控制的智能轿车停车场设计是一个集成了现代电子技术、自动化技术、计算机技术的综合性项目。该设计旨…

【QT学习】1.qt初识,创建qt工程,使用按钮,第一个交互按钮

1.初识qt--》qt是个框架&#xff0c;不是语言 1.学习路径 一 QT简介 &#xff0c;QTCreator &#xff0c;QT工程 &#xff0c;QT的第一个程序&#xff0c;类&#xff0c;组件 二 信号与槽 三 对话框 四 QT Desiner 控件 布局 样式 五 事件 六 GUI绘图 七 文件 八 …

linux监控命令全

1.1 top 1.1.1 命令说明 Top 命令能够实时监控系统的运行状态&#xff0c;并且可以按照cpu、内存和执行时间进行排序 1.1.2 用法 top -hv | -bcisSHM -d delay -n iterations [-u user | -U user] -p pid [,pid ...] 1.1.3 参数说明 命令行启动参数&#xff1a; -b : 批次…

YPay源支付V7开源版

YPay_V7版本即将停止维护更新&#xff0c;同时我们将开放最新版开源代码供学习和参考。虽然首批阶段的【function_8.1.php文件是加密的】&#xff0c;但授权已经除去&#xff0c;该代码将在新版YPay上线时开放给大家。我们也会定期进行迭代更新&#xff0c;随后将创建对应仓库&…

ImportError: cannot import name ‘PILLOW_VERSION‘ from ‘PIL‘

原因&#xff1a;torchvision模块在运行时要调用PIL模块的PILLOW_VERSION函数&#xff0c;但PILLOW_VERSION在Pillow 7.0.0之后的版本被移除了&#xff0c;Pillow 7.0.0之后的版本使用__version__函数代替PILLOW_VERSION函数。 解决方法&#xff1a;降低pillow版本即可。 参考…

梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码

源码简介 最新梨花带雨网页音乐播放器二开优化修复美化版全开源版本源码下载 梨花带雨播放器基于thinkphp6开发的XPlayerHTML5网页播放器前台控制面板,支持多音乐平台音乐解析。二开内容&#xff1a;修复播放器接口问题&#xff0c;把接口本地化&#xff0c;但是集成外链播放器…

如何提bug?

很多公司都有提bug的标准&#xff0c;对于新人刚介入测试行业时&#xff0c;提bug的时候&#xff0c;描述的清晰与否就很重要&#xff0c;那一个很明朗清晰的bug应该包含那些呢&#xff1f; bug包含的要素有那些&#xff1f;&#xff08;以jira工具为例&#xff09; 1、项目名…

Topaz Gigapixel AI for Mac 图像放大软件

Topaz Gigapixel AI for Mac是一款专为Mac用户设计的智能图像放大软件。它采用了人工智能技术&#xff0c;特别是深度学习算法&#xff0c;以提高图像的分辨率和质量&#xff0c;使得图像在放大后仍能保持清晰的细节。这款软件的特点在于其能够将低分辨率的图片放大至高分辨率&…

电商搬家接口 一键复制商品信息Python php jason

随着电商行业的迅猛发展&#xff0c;越来越多的商家开始将目光投向了线上市场。然而&#xff0c;在电商平台上运营店铺并非易事&#xff0c;尤其是在商品信息的管理与营销方面。传统的商品信息录入方式不仅效率低下&#xff0c;而且容易出错&#xff0c;给商家带来了极大的困扰…

阿里云轻量应用服务器优惠价格表,61元和165元一年

阿里云轻量应用服务器2核2G和2核4G配置优惠价格表&#xff0c;轻量2核2G3M带宽61元一年&#xff0c;轻量2核4G4M带宽165元1年&#xff0c;均不限制月流量&#xff0c;阿里云活动链接 aliyunfuwuqi.com/go/aliyun 活动打开如下图&#xff1a; 阿里云轻量应用服务器价格 61元/年…

人工智能|推荐系统——搜索引擎广告

原文题目 Dark sides of artificial intelligence: The dangers of automated decision-making in search engine advertising(JASIST,2023) 人工智能的阴暗面:搜索引擎广告自动决策的危险 摘要 随着人工智能应用的日益广泛,搜索引擎供应商越来越多地要求广告商使用基于机…

LeetCode-73. 矩阵置零【数组 哈希表 矩阵】

LeetCode-73. 矩阵置零【数组 哈希表 矩阵】 题目描述&#xff1a;解题思路一&#xff1a;用 O(mn)额外空间&#xff0c;两遍扫matrix,第一遍用集合记录哪些行,哪些列有0;第二遍置0解题思路二&#xff1a;用O(1)空间&#xff0c;关键思想: 用matrix第一行和第一列记录该行该列是…

鸿蒙OS开发实例:【埋点探究】

背景 大多数软件产品上线前&#xff0c;都会采用有规则的日志来对软件进行相关数据的采集&#xff0c;这个过程称为&#xff1a;埋点&#xff0c;采集的数据主要用于产品分析。 埋点技术已在PC端, 移动端非常成熟&#xff0c;并且有大批量以此为生的公司。 本篇将探究一下Ha…

【目标检测】YOLOv5 网络结构,bottleneckCSP 与 C3 模块图解

文章目录 Focus 模块Csp 模块BottleneckCspC3CSP1_X 与 CSP_2XYOLOv4 的 CSP_X SSP 与 SSPF YOLOv5 作为 YOLO 家族的第五个版本&#xff0c;本身也演进了几个子版本&#xff0c;现在网上的资料都没有标注具体是哪个子版本的&#xff0c;导致不同文章之间各种混乱&#xff0c;像…