Go 源码之互斥锁 Mutex

文章目录

  • 一、总结
  • 二、源码
    • (一)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.rhkb.cn/news/294583.html

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

相关文章

mongodb sharding分片模式的集群数据库,日志治理缺失导致写入数据库报错MongoWriteConcernException的问题总结(上)

一、背景 常见的mongodb集群模式有以下三种&#xff1a; 主从复制&#xff08;Master-Slave&#xff09;模式副本集&#xff08;Replica Set&#xff09;模式分片&#xff08;Sharding&#xff09;模式 公司测试环境搭建的集群采用分片模式&#xff0c;有同事反馈说&#xf…

数学矩阵(详解)

矩阵乘法 知阵乘法是《线性代数》中的基础内容&#xff0c;但在考察数学的算法题中也会出现。 本节我们学习基础的矩阵乘法规则。 每个矩阵会有一个行数和一个列数&#xff0c;只有当相乘的两个矩阵的左矩阵的列数等于右矩阵的行数 时&#xff0c;才能相乘&#xff0c;否则不允…

HCIA复习

OSI --开放式系统互联参考模型 --- 7层参考模型 TCP/IP协议栈道 --- 4层或5层 OSI&#xff1a; 应用层 抽象语言 -->编码 表示层 编码-->二进制 表示层以下都是二进制-----data&#xff08;数据&#xff09; 会话层 提供应用程序的会话地址 上三层为应用…

Go-Gin中优雅的实现参数校验,自定义错误消息提示

问题描述 在参数校验的时候我们一般会基于"github.com/go-playground/validator/v10"这个库给结构体加标签实现校验参数&#xff0c;当参数校验错误的时候&#xff0c;他的提示一般是英文的&#xff0c;怎么自定义参数错误提示呢&#xff1f;跟着我一步步来 注册校…

分析:两种不同的函数模板写法,其中一种为何不行

接上篇&#xff1a; 利用类型&#xff0c;做函数模板的“重载”-CSDN博客 比较两种模板的写法 为什么左边不可行&#xff1a; 注意&#xff0c;左边的写法的第二个模板参数&#xff0c;是默认参数的形式。为何这里采取了默认参数的形式呢&#xff0c;本意是想让编译器来走sfi…

扫地机器人(蓝桥杯)

文章目录 扫地机器人题目描述解题思路二分贪心 扫地机器人 题目描述 小明公司的办公区有一条长长的走廊&#xff0c;由 N 个方格区域组成&#xff0c;如下图所 示。 走廊内部署了 K 台扫地机器人&#xff0c;其中第 i 台在第 Ai 个方格区域中。已知扫地机器人每分钟可以移动…

互联网轻量级框架整合之JavaEE基础I

不得不解释得几个概念 JavaEE SUN公司提出来的企业版Java开发中间件&#xff0c;主要用于企业级互联网系统的框架搭建&#xff0c;同时因为Java语言优质的平台无关性、可移植性、健壮性、支持多线程和安全性等优势&#xff0c;其迅速成为构建企业互联网平台的主流技术&#x…

Oracle EBS AR接口和OM销售订单单价为空数据修复

最近,用户使用客制化Web ADI 批量导入销售订单行功能,把销售订单行的单价更新成空值,直到发运确认以后,财务与客户对帐才发现大量销售订单的单价空,同时我们检查AR接口发现销售订单的单价和金额均为空。 前提条件 采用PAC成本方式具体问题症状 销售订单行的单价为空 Path:…

【检索增强】Retrieval-Augmented Generation for Large Language Models:A Survey

本文简介 1、对最先进水平RAG进行了全面和系统的回顾&#xff0c;通过包括朴素RAG、高级RAG和模块化RAG在内的范式描述了它的演变。这篇综述的背景下&#xff0c;更广泛的范围内的法学硕士研究RAG的景观。 2、确定并讨论了RAG过程中不可或缺的核心技术&#xff0c;特别关注“…

C# WPF编程-Application类(生命周期、程序集资源、本地化)

C# WPF编程-Application类 应用程序的生命周期创建Application对象应用程序的关闭方式应用程序事件 Application类的任务显示初始界面处理命令行参数访问当前Application对象在窗口之间进行交互 程序集资源添加资源检索资源pack URI内容文件 每个运行中的WPF应用程序都由System…

uniapp 开发之原生Android插件

开发须知 在您阅读此文档时&#xff0c;我们假定您已经具备了相应Android应用开发经验&#xff0c;使用Android Studio开发过Android原生。也应该对HTML,JavaScript,CSS等有一定的了解, 并且熟悉在JavaScript和JAVA环境下的JSON格式数据操作等。 为了插件开发者更方便快捷的开…

在Windows的Docker上部署Mysql服务

在我们做一些和数据库相关的测试时&#xff0c;往往需要快速部署一个数据库作为数据源。如果开发环境是Windows&#xff0c;且开发的代码不依赖于系统&#xff0c;即不用在linux上做开发&#xff0c;则可以将全套环境都部署在Windows上。 本地安装数据库会污染操作系统环境&…

【学习笔记】java项目—苍穹外卖day03

文章目录 苍穹外卖-day03课程内容1. 公共字段自动填充1.1 问题分析1.2 实现思路1.3 代码开发1.3.1 步骤一1.3.2 步骤二1.3.3 步骤三 1.4 功能测试1.5 代码提交 2. 新增菜品2.1 需求分析与设计2.1.1 产品原型2.1.2 接口设计2.1.3 表设计 2.2 代码开发2.2.1 文件上传实现2.2.2 新…

test02

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

Linux shell编程学习笔记45:uname命令-获取Linux系统信息

0 前言 linux 有多个发行版本&#xff0c;不同的版本都有自己的版本号。 如何知道自己使用的Linux的系统信息呢&#xff1f; 使用uname命令、hostnamectl命令&#xff0c;或者通过查看/proc/version文件来了解这些信息。 我们先看看uname命令。 1 uname 命令的功能和格式 …

Java

1.学生和老师都会有work方法&#xff0c;学生的工作是学习&#xff0c;老师的工作是教书&#xff0c;我利用了一个接口来实现&#xff1b; 2.同时&#xff0c;老师和学生都是人&#xff0c;并且都有姓名&#xff0c;姓名&#xff0c;年龄和身高等特征&#xff0c;我用了一个继承…

加密软件VMProtect教程:使用脚本-功能

VMProtect是新一代软件保护实用程序。VMProtect支持德尔菲、Borland C Builder、Visual C/C、Visual Basic&#xff08;本机&#xff09;、Virtual Pascal和XCode编译器。 同时&#xff0c;VMProtect有一个内置的反汇编程序&#xff0c;可以与Windows和Mac OS X可执行文件一起…

RabbitMQ高级笔记

视频链接&#xff1a;【黑马程序员RabbitMQ入门到实战教程】 文章目录 1.发送者的可靠性1.1.生产者重试机制1.2.生产者确认机制1.3.实现生产者确认1.3.1.开启生产者确认1.3.2.定义ReturnCallback1.3.3.定义ConfirmCallback 2.MQ的可靠性2.1.数据持久化2.1.1.交换机持久化2.1.2.…

Docker搭建LNMP环境实战(09):安装mariadb

1、编写mariadb部署配置文件 在文件夹&#xff1a;/mnt/hgfs/dockers/test_site/compose下创建文件&#xff1a;test_site_mariadb.yml&#xff0c;内容如下&#xff1a; version: "3.5" services:test_site_mariadb:container_name: test_site_mariadbimage: mari…

代码+视频,手动绘制logistic回归预测模型校准曲线(Calibration curve)(1)

校准曲线图表示的是预测值和实际值的差距&#xff0c;作为预测模型的重要部分&#xff0c;目前很多函数能绘制校准曲线。 一般分为两种&#xff0c;一种是通过Hosmer-Lemeshow检验&#xff0c;把P值分为10等分&#xff0c;求出每等分的预测值和实际值的差距. 另外一种是calibra…