Go 源码之互斥锁 Mutex

news2025/1/15 20:47:51

文章目录

  • 一、总结
  • 二、源码
    • (一)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()中不再唤醒其他waiter
			if !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 := old
		
		if 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 != 0
			if waitStartTime == 0 {                       // 判断是不是第一次加入队列
				waitStartTime = runtime_nanotime()         // 如果之前就在队列里面等待了,加入到对头
			}        
			runtime_SemacquireMutex(&m.sema, queueLifo, 1) // 阻塞等待
          // 检查锁是否处于饥饿状态
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
          // 如果锁处于饥饿状态,直接抢到锁
			if old&mutexStarving != 0 {
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
              //设置标志,进行加锁并且waiter-1
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				//如果是最后一个的话清除饥饿标志
              if !starving || old>>mutexWaiterShift == 1 {
                 //退出饥饿模式				
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 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 := new
		for {
			// 正常模式下,没有waiter或者在处理事情的情况下直接返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			//如果有等待者,设置mutexWoken标志,waiter-1,更新state
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if 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 = 4
    if 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.coloradmin.cn/o/1569687.html

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

相关文章

2.Swift基础控件:图标文字按钮

Swift图标标题按钮 一、自定义IconTitleButton类 import Foundation/* 枚举 设置 图片的位置 */ enum ButtonImagePosition : Int {case imageTop 0case imageLeftcase imageBottomcase imageRight } extension UIButton {/**type &#xff1a;image 的位置Space &#xff1…

代码审计-PHP原生开发篇SQL注入数据库监控正则搜索文件定位静态分析

文章目录 前言1、Bluecms-CNVD-1Day-常规注入审计分析2、emlog-CNVD-1Day-常规注入审计分析3、emlog-CNVD-1Day-2次注入审计分析 前言 挖掘技巧&#xff1a; -语句监控-数据库SQL监控排查可利用语句定向分析 -功能追踪-功能点文件SQL执行代码函数调用链追踪 -正则搜索-(update…

时序预测 | Matlab实现SOM-BP自组织映射结合BP神经网络时间序列预测

时序预测 | Matlab实现SOM-BP自组织映射结合BP神经网络时间序列预测 目录 时序预测 | Matlab实现SOM-BP自组织映射结合BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现SOM-BP自组织映射结合BP神经网络时间序列预测&#xff08;完整源码…

Flutter-发布插件到pub上传不上问题

问题1&#xff1a; 尝试指令&#xff1a; flutter packages pub publish --serverhttps://pub.dartlang.org问题2&#xff1a; 问题1解决后&#xff0c;进入验证身份&#xff0c;点击终端显示的链接&#xff0c;跳转到google验证&#xff0c;记得这里要科*学上网&#xff0c;点…

Collection与数据结构 链表与LinkedList(三):链表精选OJ例题(下)

1. 分割链表 OJ链接 class Solution {public ListNode partition(ListNode head, int x) {if(head null){return null;//空链表的情况}ListNode cur head;ListNode formerhead null;ListNode formerend null;ListNode latterhead null;ListNode latterend null;//定义…

阿里云服务器配置CPU内存带宽ECS规格如何选择?8000元预算

8000元预算如何选择阿里云服务器配置&#xff1f;八千预算可选的阿里云服务器配置相当高了&#xff0c;这个预算可以购买阿里云企业级独享型云服务器&#xff0c;至少8核以上的配置&#xff0c;这个预算可以支持复杂、高负载或大规模的业务需求。阿里云服务器网整理8000元/月预…

pycharm和Spyder多行注释快捷键

1.选取注释内容 2.pycharm&#xff1a;使用Ctrl/ 3.Spyder&#xff1a;使用Ctrl1 效果图

数字乡村创新实践探索:科技赋能农业现代化与乡村治理体系现代化同步推进

随着信息技术的飞速发展&#xff0c;数字乡村作为乡村振兴的重要战略方向&#xff0c;正日益成为推动农业现代化和乡村治理体系现代化的关键力量。科技赋能下的数字乡村&#xff0c;不仅提高了农业生产的效率和品质&#xff0c;也为乡村治理带来了新的机遇和挑战。本文旨在探讨…

接口调用成功后端却一直返回404

vuespringboot 我在vue.config.js中配置了向后端的反向代理 然后使用了axios向后端发送post请求 可以看到可以接收到前端传来的值 但是前端控制台却报了 “xhr.js:245POST http://localhost:7777/api/login 404 (Not Found)” 最后询问我那智慧的堂哥... ... 解决办法是把C…

DFS:深搜+回溯+剪枝解决矩阵搜索问题

创作不易&#xff0c;感谢三连&#xff01;&#xff01; 一、N皇后 . - 力扣&#xff08;LeetCode&#xff09; class Solution { public:vector<vector<string>> ret;vector<string> path;bool checkcol[9];bool checkdig1[18];bool checkdig2[18];int n…

Chat-REC: Towards Interactive and Explainable LLMs-Augmented Recommender System

1、动机 推荐系统被应用于推荐服务&#xff0c;提高人们的生活质量&#xff0c;但仍存在一些问题。缺少交互性、可解释性&#xff0c;缺乏反馈机制&#xff0c;以及冷启动和跨域推荐。 Chat-Rec 将用户画像和历史交互转换为 Prompt&#xff0c;有效地学习用户的偏好&#xff…

Chapter 1 - 9. Introduction to Congestion in Storage Networks

Terminology iSCSI uses the terminology of initiator (or iSCSI client) and target (or iSCSI server). NVMe/TCP uses the terminology of host and controller respectively. iSCSI 使用的术语是启动程序(或 iSCSI 客户端)和目标程序(或 iSCSI 服务器)。NVMe/TCP 分别…

GIS水文分析填充伪洼地学习

1 基本操作 洼地是指流域内被较高高程所包围的局部区域&#xff1b; 分为自然洼地和伪洼地&#xff1b; 自然洼地是自然界实际存在的洼地&#xff1b; 在 DEM 数据中&#xff0c;由于数据处理的误差和不合适的插值方法所产生的洼地&#xff0c;称为伪洼地&#xff1b; DEM 数据…

Day80:服务攻防-中间件安全HW2023-WPS分析WeblogicJettyJenkinsCVE

目录 中间件-Jetty-CVE&信息泄漏 CVE-2021-34429(信息泄露) CVE-2021-28169(信息泄露) 中间件-Jenkins-CVE&RCE执行 cve_2017_1000353 CVE-2018-1000861 cve_2019_1003000 中间件-Weblogic-CVE&反序列化&RCE 应用金山WPS-HW2023-RCE&复现&上线…

win10 安装kubectl,配置config连接k8s集群

安装kubectl 按照官方文档安装&#xff1a;https://kubernetes.io/docs/tasks/tools/install-kubectl-windows/ curl安装 &#xff08;1&#xff09;下载curl安装压缩包: curl for Windows &#xff08;2&#xff09;配置环境变量&#xff1a; 用户变量&#xff1a; Path变…

LeetCode:331. 验证二叉树的前序序列化(模拟 Java)

目录 331. 验证二叉树的前序序列化 题目描述&#xff1a; 实现代码与解析&#xff1a; 模拟 原理思路&#xff1a; 331. 验证二叉树的前序序列化 题目描述&#xff1a; 序列化二叉树的一种方法是使用 前序遍历 。当我们遇到一个非空节点时&#xff0c;我们可以记录下这个节…

Samba 总是需要输入网络凭证

输入网络凭证&#xff1a; 用户名是 cat /etc/samba/smb.conf&#xff0c;查看 valid users mxw 为用户名。而不是其他账号名或者用户名&#xff0c;更不是登录计算机时的计算机名&#xff1b; 密码是 需要记住安装samba服务器时&#xff0c;自己设置的password&#xff1…

zdpcss_transparent_animation_login:使用纯HTML+CSS+JS开发支持设置主题和带动画的科技风登录界面

废话不多说&#xff0c;先上图&#xff0c;有图有真相&#xff1a; 在左下角有一排颜色&#xff0c;点击可以设置主题色&#xff1a; 比如&#xff0c;我这里点击了橙色&#xff0c;登录界面就变成了如下样子&#xff1a; 另外&#xff0c;在输入账号和密码的时候&#x…

设计模式——工厂模式01

工厂模式 定义&#xff1a;工厂模式是创建子类实例化对象的一种方式&#xff0c;屏蔽了创造工厂的内部细节。把创建对象与使用对象进行拆分&#xff0c;满足单一职责。如果需要向工厂中添加新商品&#xff0c; 只需要扩展子类再重写其工厂方法&#xff0c;满足开闭原则。 设计…

STM32-05基于HAL库(CubeMX+MDK+Proteus)串行通信案例(中断方式接收命令)

文章目录 一、功能需求分析二、Proteus绘制电路原理图三、STMCubeMX 配置引脚及模式&#xff0c;生成代码四、MDK打开生成项目&#xff0c;编写HAL库的功能代码五、运行仿真程序&#xff0c;调试代码 一、功能需求分析 在中断机制实现按键检测的案例之后&#xff0c;我们介绍串…