当前位置: 首页 > news >正文

【重造轮子】golang实现可重入锁

造个可重入锁的轮子

    • 介绍
      • 目标
    • 正文
      • sync.Mutex
        • sync.Mutex介绍
          • 多协程计数器demo
          • 多协程计数器加锁
        • 源码剖析
          • Mutex数据结构
          • Lock()
            • 加锁核心逻辑
          • UnLock()
      • 重入锁与可重入锁
      • 魔改 sync.Mutex
    • 参考文档

介绍

开新坑啦!!
从这篇文章开始,尝试造轮子,包括一些可能有用、也可能没用的轮子。
温故而知新,我相信时常回顾基础的东西能让我们受益良多,这点我深有体会,每过一段时间我都会把《程序员的自我修养》拿出来翻翻,常翻常新,每次读都能有新的收获,开始吧。

“转向毕竟是一个很长的过程,先做起来吧,给我和别的生命一个活下去的机会。”—《三体》

目标

用go 实现可重入锁;

正文

Golang的sync.Mutex是并发场景下的“灵丹妙药”,但是我们真的了解吗?
本文通过对源码的剖析,让我们重新、更全面的认识sync.Mutex,尤其是其优缺点。
在对sync.Mutex有了深入了解后,我们尝试对其进行魔改,实现可重入锁。

sync.Mutex

点击上方的sync.Mutex进入golang.tour我们可以看到,sync.Mutex的简单介绍;
总结如下:

sync.Mutex介绍

当多个goroutine之间需要通信,尤其需要访问(同一份)数据时,需要互斥(锁)来保证一次只有一个goroutine访问数据。
当然,sync.Mutex作为一个同步原语实现了Locker接口(后面会提到),所以只有Lock()、Unlock()两个接口。

多协程计数器demo

比如大家在许多地方看到的例子,多协程累加计数:

var (counter = 1
)func incrCounter() {var wg sync.WaitGroupwg.Add(11)i := 0for {go func() {defer wg.Done()iter:=0for {counter ++if iter > 4 {break}iter ++}}()if i > 9 {break}i ++}wg.Wait()fmt.Println("incrCounter:",counter)os.Exit(1)
}

本地运行结果是incrCounter: 56;大家可以试试本地运行的结果。

多协程计数器加锁

接下来就说到今天的主角了;在上面的多协程计数器代码上,集成sync.Mutex,加两行代码,分别是Lock、Unlock;
代码如下


var (mtx sync.Mutexcounter = 1
)
func incrCounter() {var wg sync.WaitGroupwg.Add(11)i := 0for {go func() {defer wg.Done()iter:=0for {mtx.Lock() counter ++mtx.Unlock()if iter > 4 {break}iter ++}}()if i > 9 {break}i ++}wg.Wait()fmt.Println("incrCounter:",counter)os.Exit(1)
}

本地运行结果是incrCounter: 67;大家可以试试本地运行的结果。(相信很多人看到67会觉得奇怪,没错我是故意的,就是给粗心的同学卖了一个坑,想想为什么是67而不是66?!)
奇怪哎,为啥加了sync.Mutex不一样了呢?!
这里一定要回顾下开头sync.Mutex的介绍!

源码剖析

点击sync.Mutex,我们可以看到它的数据结构;

Mutex数据结构

简单解释下分别是:
state状态位(如果不是远古版本,分为了4段),以及sema信号量变量;不急,后面会细说,这里先了解基本构成;

type Mutex struct {state int32sema  uint32
}

回顾多协程计数器中sync.Mutex的使用例子,核心方法只有两个,为什么只有两个呢?看源码发现原来是实现了Locker interface,因为实现了Locker所以有Lock()、UnLock()
这里需要重点说一下,golang中的同步原语都会实现Locker ,比如RWMutex;所以以后提到Lock、UnLock那么就可以思考是不是实现了Locker interface;

// A Locker represents an object that can be locked and unlocked.
type Locker interface {Lock()Unlock()
}

接下来,看下Lock()的实现;看看golang是如何加锁的。

Lock()

照例,点进去看下源码;
如果没加锁,运气很好,加锁就行然后返回;如果已经加过锁了,那么就进入lockSlow,也是加锁逻辑最复杂的地方;
race是做死锁检查的,先不管,捋主体逻辑先;
这里多提一下,fast path一般用来表示捷径或者幸运case,意思是直接成功,不用再执行复杂的逻辑,如果大家看多了开源项目看到fast path就可以跳过这段代码,因为不用看你也能猜到这段代码的意思;

func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path (outlined so that the fast path can be inlined)m.lockSlow()
}
加锁核心逻辑

先看几个变量;
表示饥饿模式的starving,
唤醒状态的标记awoke,
迭代次数统计的iter,
当前的加锁状态old;

var waitStartTime int64starving := falseawoke := falseiter := 0old := m.state

接下来是饥饿模式的自旋逻辑;

if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// Active spinning makes sense.// Try to set mutexWoken flag to inform Unlock// to not wake other blocked goroutines.if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++old = m.statecontinue}

如果是饥饿模式,那么直接拿到锁,新到的goroutine会放入等待队列(等待队列数+1);

new := old
if old&mutexStarving == 0 {new |= mutexLocked}
if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}

如果当前协程是饥饿模式,并且Mutex并没有标记为饥饿模式,那么就把Mutex标记为饥饿模式;如果已被唤醒那么就标记为已唤醒状态;

if starving && old&mutexLocked != 0 {new |= mutexStarving}
if awoke {// The goroutine has been woken from sleep,// so we need to reset the flag in either case.if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}

紧接着,将改变的状态同步到Mutex.state字段;
如果到目前为止当前协程没有获取到锁也没有进入饥饿模式,就可以提前结束当前流程(等待下一次唤醒);
判断运行时间,并调用runtime_SemacquireMutex休眠,并尝试获取信号量;
运行时间超过1ms就自动进入饥饿模式(starving = 1);
一旦当前Mutex被标记为饥饿模式,将状态保存到Mutex.state中;
这里需要注意state(int32)中各段的:
第一段(最左边29位)为等待协程的数量;
第二段(1位)饥饿模式标记;
第三段(1位)唤醒标记;
第四段(1位)是否加锁;
如果没有进入饥饿模式,那么将唤醒标记为true,并且重新开始(继续尝试获取锁);

if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break // locked the mutex with CAS}// If we were already waiting before, queue at the front of the queue.queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif old&mutexStarving != 0 {// If this goroutine was woken and mutex is in starvation mode,// ownership was handed off to us but mutex is in somewhat// inconsistent state: mutexLocked is not set and we are still// accounted as waiter. Fix that.if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {// Exit starvation mode.// Critical to do it here and consider wait time.// Starvation mode is so inefficient, that two goroutines// can go lock-step infinitely once they switch mutex// to starvation mode.delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}
UnLock()

记得前面的fast path这个case吗;如果state为1就直接释放锁然后就结束了;
这里需要结合加锁逻辑去看;
回顾下组成state的四个部分:
第一段(最左边29位)为等待协程的数量;
第二段(1位)饥饿模式标记;
第三段(1位)唤醒标记;
第四段(1位)是否加锁;
那么state为,说明:没有等待的协程,没有饥饿模式和唤醒标记,仅仅Mutex被加锁了;

func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// Fast path: drop lock bit.new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {// Outlined slow path to allow inlining the fast path.// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.m.unlockSlow(new)}
}

否则,进入unlockSlow逻辑;
入口时异常判断,如果释放一个没有加锁的锁则抛出异常;
如果是饥饿模式,将锁直接给饥饿模式的协程,注意是饥饿模式的协程不是等待队列中的等待协程
不是饥饿模式(比如正常的等待协程)是正常模式,判断锁是否已被锁定或者是否存在唤醒或者是否是饥饿模式,则直接放回,并不释放锁;否则唤醒等待队列中的协程,直接移交给等待者;

func (m *Mutex) unlockSlow(new int32) {if (new+mutexLocked)&mutexLocked == 0 {fatal("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {// If there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. We are not part of this chain,// since we did not observe mutexStarving when we unlocked the mutex above.// So get off the way.if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// Grab the right to wake someone.new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// Starving mode: handoff mutex ownership to the next waiter, and yield// our time slice so that the next waiter can start to run immediately.// Note: mutexLocked is not set, the waiter will set it after wakeup.// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.runtime_Semrelease(&m.sema, true, 1)}
}

重入锁与可重入锁

未完待续

魔改 sync.Mutex

未完待续

参考文档

认识可重入锁
Mutex
饥饿模式

相关文章:

【重造轮子】golang实现可重入锁

造个可重入锁的轮子 介绍目标 正文sync.Mutexsync.Mutex介绍多协程计数器demo多协程计数器加锁 源码剖析Mutex数据结构Lock()加锁核心逻辑 UnLock() 重入锁与可重入锁魔改 sync.Mutex 参考文档 介绍 开新坑啦&#xff01;&#xff01; 从这篇文章开始&#xff0c;尝试造轮子&a…...

torch显存分析——对生成模型清除显存

torch显存分析——对生成模型清除显存 1. 问题介绍2. 应对方法 1. 问题介绍 本文主要针对生成场景下&#xff0c;如何方便快捷地清除当前进程占用的显存。文章的重点不止是对显存的管理&#xff0c;还包括怎样灵活的使用自定义组件来控制生成过程。 在之前的文章torch显存分析…...

electron+vue+ts窗口间通信

文章目录 一. 目的二.逻辑分析三. 代码示例 "types/node": "^20.3.1","vitejs/plugin-vue": "^4.1.0","vueuse/electron": "^10.2.1","electron": "^25.2.0","electron-packager":…...

基于Fringe-Projection环形投影技术的人脸三维形状提取算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 .................................................................... figure; imshow(Im…...

如何使用Webman框架实现多语言支持和国际化功能?

如何使用Webman框架实现多语言支持和国际化功能&#xff1f; Webman是一款轻量级的PHP框架&#xff0c;提供了丰富的功能和扩展性&#xff0c;使得开发人员能够更加高效地开发Web应用程序。其中&#xff0c;多语言支持和国际化功能是Web应用程序中非常重要的一项功能&#xff…...

接受平庸,特别是程序员

目录 方向一&#xff1a;简述自己的感受 方向二&#xff1a;聊聊你想怎么做 方向三&#xff1a;如何调整自己的心态 虽然清楚知识需要靠时间沉淀&#xff0c;但在看到自己做不出来的题别人会做&#xff0c;自己写不出的代码别人会写时还是会感到焦虑怎么办&#xff1f; 你是…...

HTML兼容性

文章目录 一、兼容性二、兼容问题1. 在IE6下&#xff0c;子级的宽度会撑开父级设置好的宽度2. IE6中&#xff0c;元素浮动&#xff0c;如果宽度需要内容撑开&#xff0c;需要给里面的块元素都添加浮动才可以3. 在IE6、7下&#xff0c;元素要通过浮动排在同一排&#xff0c;就需…...

Java日期和时间处理入门指南

文章目录 1. 日期操作 - java.util.Date1.1 构造方法1.2 常用方法 2. 日期格式化 - java.text.SimpleDateFormat2.1 获取对象2.2 方法 3. 获取时间分量 - java.util.Calendar3.1 时间分量3.2 创建对象3.3 常用的时间分量3.4 获取时间分量3.5 设置时间分量 结语 引言&#xff1a…...

anndata k折交叉

如何将anndata拆分为k份 import scanpy as sc import anndata as adclass KSplitAnndata:staticmethoddef _base_split(data: object, k: int) -> list:adata data.copy()num adata.n_obs // kadata_list []for i in range(k):if num < adata.n_obs:adata_list.appen…...

深入解析项目管理中的用户流程图

介绍用户流程图 用户流程图的定义 用户流程图(User Flow Diagram)是一种可视化工具&#xff0c;它描绘了用户在应用或网站上完成任务的过程。这些任务可以是购物、注册账户、查找信息等&#xff0c;任何需要用户交互的动作都可以在用户流程图中找到。 用户流程图的重要性 用…...

Vue使用QrcodeVue生成二维码并下载

生成二维码 1、安装qrcode.vue组件 npm install --save qrcode.vue<template><div id"app"><qrcode-vue :valuevalue :sizesize></qrcode-vue><br /></div> </template><script> //导入组件 import QrcodeVue fro…...

“用户登录”测试用例总结

前言&#xff1a;作为测试工程师&#xff0c;你的目标是要保证系统在各种应用场景下的功能是符合设计要求的&#xff0c;所以你需要考虑的测试用例就需要更多、更全面。鉴于面试中经常会问“”如何测试用户登录“”&#xff0c;我们利用等价类划分、边界值分析等设计一些测试用…...

适应于Linux系统的三种安装包格式 .tar.gz、.deb、rpm

deb、rpm、tar.gz三种Linux软件包的区别 rpm包-在红帽LINUX、SUSE、Fedora可以直接进行安装&#xff0c;但在Ubuntu中却无法识别&#xff1b; deb包-是Ubuntu的专利&#xff0c;在Ubuntu中双击deb包就可以进入自动安装进程&#xff1b; tar.gz包-在所有的Linux版本中都能使用…...

Linux lvs负载均衡

LVS 介绍&#xff1a; Linux Virtual Server&#xff08;LVS&#xff09;是一个基于Linux内核的开源软件项目&#xff0c;用于构建高性能、高可用性的服务器群集。LVS通过将客户端请求分发到一组后端服务器上的不同节点来实现负载均衡&#xff0c;从而提高系统的可扩展性和可…...

Tomcat 创建https

打开CMD,按下列输入 keytool -genkeypair -alias www.bo.org -keyalg RSA -keystore d:\ambition.keystore -storetype pkcs12 输入密钥库口令:123456 再次输入新口令:123456 您的名字与姓氏是什么? [Unknown]: www.ambition.com 您的组织单位名称是什么? [Unknown…...

超导电性的基本现象和相关理论

超导体 Hg 超导电性的基本现象和相关理论 超导体的基本特性 低温零电阻突变&#xff08;< 10^{-23 \Omega/m}&#xff09; 良导体在 10^{-10} \Omega/m临界温度迈斯纳效应 完全排磁通效应&#xff08;完全抗磁性&#xff09; 超导体物体内部不存在电场 第一类超导体与第二类…...

在 PHP 中单引号(‘ ‘)和双引号(“ “)用法的区别

在 PHP 中&#xff0c;使用单引号&#xff08; &#xff09;和双引号&#xff08;" "&#xff09;可以创建字符串。这两种引号的用法有一些区别。 单引号&#xff1a; 单引号用于创建简单的字符串&#xff0c;其中的变量和转义字符将不会被解析。单引号中的任何内容…...

SpringCloudAlibaba:服务网关之Gateway的cors跨域问题

目录 一&#xff1a;解决问题 二&#xff1a;什么是跨域 三&#xff1a;cors跨域是什么&#xff1f; 一&#xff1a;解决问题 遇到错误&#xff1a; 前端请求时报错 解决&#xff1a; 网关中添加配置文件&#xff0c;注意springboot版本&#xff0c;添加配置。 springboo…...

react中的高阶组件理解与使用

一、什么是高阶组件&#xff1f; 其实就是一个函数&#xff0c;参数是一个组件&#xff0c;经过这个函数的处理返回一个功能增加的组件。 二、代码中如何使用 1&#xff0c;高级组件headerHoc 2&#xff0c;在普通组件header中引入高阶组件并导出高阶组件&#xff0c;参数是普…...

“从零开始学习Spring Boot:构建高效的Java应用程序“

标题&#xff1a;从零开始学习Spring Boot&#xff1a;构建高效的Java应用程序 摘要&#xff1a;本篇博客将带你从零开始学习如何使用Spring Boot构建高效的Java应用程序。我们将讨论Spring Boot的基本概念和特性&#xff0c;并提供一个简单的示例代码来帮助你入门。 正文&am…...

容器部署jenkins定时构建于本地时间不一致

1. Dockerfile FROM jenkins/jenkins:2.411-jdk11 USER root #以下生成密钥方式为旧格式&#xff0c;因为新格式暂不能被"Publish over SSH--->Jenkins SSH Key"功能识别 RUN ssh-keygen -q -m PEM -t rsa -b 2048 -N -f /root/.ssh/id_rsa ADD ./apache-maven…...

生成指定网段的IP字典自动化脚本

目录 1.前言 2.生成指定网段的IP字典自动化脚本 1.前言 在可回显的服务端跨站请求伪造(SSRF)漏洞中,我们通常会利用该漏洞进行内网资产探测。最近正好碰到了。写了一个小脚本。 2.生成指定网段的IP字典自动化脚本 脚本可指定协议、IP段、和端口生成字典。 get-Intranet-A…...

Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单 em

&#xfeff; Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&…...

《向量数据库指南》——大模型时代,为什么向量数据库成为标配?

目录 1. 数据持久化和低成本存储 2. 高性能查询 3. 数据分布 4. 易于使用 5. 稳定可用 6. 可运维可观测 7. 智能化 1. 数据持久化和低成本存储 许多单机和轻量级的向量数据库并没有关注数据的可靠性,Milvus Cloud 基于对象存储和消息队列的存储方案既通过存储计算分离…...

Pytorch个人学习记录总结 10

目录 优化器 优化器 官方文档地址&#xff1a;torch.optimhttps://pytorch.org/docs/stable/optim.html Debug过程中查看的grad所在的位置&#xff1a; model --> Protected Atributes --> _modules --> ‘model’ --> Protected Atributes --> _modules -…...

18款奔驰S320升级后排座椅加热功能,提升后排乘坐舒适性

奔驰座椅加热就简单多了&#xff0c;是在原车座椅海绵表面安装一层加热垫&#xff0c;加热垫里面是加热丝&#xff0c;通过电机热的原理&#xff0c;快速升温&#xff0c;把热量传递给车主。 奔驰的座椅加热系统是通过车门按键来控制&#xff0c;3档调节&#xff0c;温度从低到…...

Vue中的插值表达式

Vue中的插值表达式&#xff08;Interpolation&#xff09;用于将数据动态绑定到HTML模板中。它的主要作用是在模板中直接显示变量的值&#xff0c;并实现数据的双向绑定。以下是插值表达式的一些作用&#xff1a; 1.变量展示&#xff1a;插值表达式允许将Vue实例中的数据直接显…...

背包问题(模板)

目录 01背包&#xff1a; 完全背包&#xff1a; 多重背包&#xff08;范围0-100&#xff09;&#xff1a; 混合背包&#xff1a; 分组背包&#xff1a; 二维费用的背包问题&#xff1a; 背包问题求方案数&#xff1a; 01背包&#xff1a; 从最大容量开始遍历到当前&…...

docker容器创建私有仓库(第三篇)

目录 六、创建私有仓库 七、Docker资源限制 7.1、CPU使用率 7.2、CPU共享比例 7.3、CPU周期限制 7.4、CPU核心限制 7.5、CPU 配额控制参数的混合案例 7.6、内存限制 7.7、Block IO 的限制 7.8、限制bps 和iops 8、Docker数据持久化 8.1、数据持久化介绍 8.2、Volum…...

Eureka 学习笔记4:客户端 DiscoveryClient

版本 awsVersion ‘1.11.277’ DiscoveryClient # cacheRefreshTask // 配置shouldFetchRegistry if (clientConfig.shouldFetchRegistry()) {// 配置client.refresh.intervalint registryFetchIntervalSeconds clientConfig.getRegistryFetchIntervalSeconds();// 配置expB…...