【重造轮子】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
饥饿模式

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

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

相关文章

目标检测任务中常用的数据集格式(voc、coco、yolo)

一、Pascal VOC VOC数据集(Annotation的格式是xmI) Pascal VOC数据集是目标检测的常用的大规模数据集之一&#xff0c;从05年到12年都会举办比赛&#xff0c;比赛任务task&#xff1a; 分类Classification目标检测Object Detection语义分割Class Segmentation实例分割Object…

基于PHP校园疫情防控信息管理系统-计算机毕设 附源码12057

PHP校园疫情防控信息管理系统 摘 要 如今计算机行业的发展极为快速&#xff0c;搭载于计算机软件运行的数据库管理系统在各行各业得到了广泛的运用&#xff0c;其在数据管理方面具有的准确性和高效性为大中小企业的日常运营提供了巨大的帮助。自从2020年新冠疫情爆发以来&…

ES6及以上新特性

ES6&#xff08;ECMAScript 2015&#xff09;及以上版本引入了许多新特性&#xff0c;每个版本都有不同的增强和改进。以下是 ES6 及以上版本的新特性的详细描述&#xff1a; ES6&#xff08;ECMAScript 2015&#xff09;&#xff1a; let 和 const 声明&#xff1a;引入块级作…

【嵌入式学习笔记】嵌入式入门2——中断(外部中断)

1.什么是中断 打断CPU执行正常的程序&#xff0c;转而处理紧急程序&#xff0c;然后返回原暂停的程序继续运行&#xff0c;就叫中断 1.1.中断的作用与意义 作用1&#xff1a;实时控制在确定时间内对相应事件作出响应——定时器中断作用2&#xff1a;故障处理检测到故障&…

Makefile模板和工程模板(消息队列和共享内存)的使用

一、 Makefile模板 #指定生成的文件名 OJB_OUT test#指定每一个c文件对应的.o文件 OBJS a.o b.o main.o#指定编译器 CC gcc#指定需要的库 ULDFLAGS ########################################### #以下的内容不需要修改 ########################################### all:…

NASA和uAvionix在AAM测试场部署SkyLine C2指挥和控制服务

蒙大拿州比格福克和弗吉尼亚州汉普顿2023年07月28日——美国宇航局和uAvionix签署了一项太空法案协议&#xff0c;为城市环境中的无人机系统 (UAS)开发先进的超视距(BVLOS)指挥和控制(C2)技术。根据协议&#xff0c;NASA将与uAvionix合作&#xff0c;利用基于互联网的基础设施和…

(二)Spring WeFlux响应式编程第二种整合方案|道法术器

Spring WebFlux 响应式异步编程|道法术器(一) Spring WeFlux响应式编程整合另一种方案|道法术器(二) R2dbc操作mysql 注意下面红色部分与上一篇"Spring WebFlux 响应式异步编程|道法术器(一)" 不一样的依赖包 技术整合: <!--设置spring-boot依赖的版本 --> &l…

kafka部署

1.kafka安装部署 1.1 kafaka下载 https://archive.apache.org/dist/kafka/2.4.0/kafka_2.12-2.4.0.tgz Binary downloads是指预编译的软件包,可供直接下载和安装,无需手动编译。在计算机领域中,二进制下载通常指预构建的软件分发包,可以直接安装在系统上并使用 "2.…

麒麟-飞腾Kylin-V4桌面arm64系统静态编译QT

1.系统具体版本&#xff1a; 2. 因为此版本的源很老了&#xff0c;需要修改版本的源&#xff0c;才能正常更新各种软件&#xff0c;否则&#xff0c;你连麒麟商店都打不开。 sudo vi /etc/apt/sources.list 选择你系统对应版本的源地址&#xff1a; #4.0.2桌面版本: deb ht…

django后台系统Tyadmin

无意之间发现个django的后台管理框架&#xff0c;仔细与xadmin对比了一下&#xff0c;无论是功能上还是便携性上都与xadmin特别相似&#xff0c;但个人感觉Tyadmin略胜一筹&#xff0c;因为外观上要比xadmin要美观&#xff0c;而且相比起来速度也快&#xff0c;部署甚至也和简单…

ModuleNotFoundError: No module named ‘_sqlite3‘

前言 遇到报错信息如下&#xff1a; ModuleNotFoundError: No module named _sqlite3解决方式 参考解决方式&#xff1a; https://blog.csdn.net/jaket5219999/article/details/53512071 find / -name _sqlite*.socp /usr/lib64/python3.6/lib-dynload/_sqlite3.cpython-36…

Eureka 学习笔记1:服务端实例缓存

版本 awsVersion ‘1.11.277’ 缓存类型registryConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>AbstractInstanceRegistry成员变量readWriteCacheMapLoadingCacheResponseCacheImpl成员变量readOnlyCacheMapConcurrentMap<Key, Value>…

uniapp 路由跳转方式

export function goBack(index, url) {if (index 1) { // 关闭当前页&#xff0c;返回上一页面或多级页面。uni.navigateBack({delta: url,animationType: pop-out,animationDuration: 300});} else if (index 2) { // 保留当前页&#xff0c;跳转到非tabbar页面&#xff0c;…

vue拖拽改变宽度

1.封装组件ResizeBox.vue <template><div ref"resize" class"resize"><div ref"resizeHandle" class"handle-resize" /><slot /></div> </template> <script> export default {name: Resi…

Ubuntu的tar命令详解

在 Ubuntu 中压缩文件夹可以使用 tar 命令。tar 可以将多个文件或文件夹打成一个包&#xff0c;并可选是否进行压缩&#xff0c;最常用的压缩方式是 gzip 和 bzip2。 常用的 tar 命令参数如下&#xff1a; -c&#xff1a;创建新的 tar 包&#xff1b; -x&#xff1a;解压 tar…

【Docker】使用docker-maven-plugin插件构建发布推镜像到私有仓库

文章目录 1. 用docker-maven-plugin插件推送项目到私服docker1.1. 构建镜像 v1.01.2. 构建镜像 v2.01.3. 推送到镜像仓库 2. 拉取私服docker镜像运行3. 参考资料 本文描述了在Spring Boot项目中通过docker-maven-plugin插件把项目推送到私有docker仓库中&#xff0c;随后拉取仓…

四大软件测试策略的特点和区别(单元测试、集成测试、确认测试和系统测试)

四大软件测试策略分别是单元测试、集成测试、确认测试和系统测试。 一、单元测试 单元测试也称为模块测试&#xff0c;它针对软件中的最小单元&#xff08;如函数、方法、类、模块等&#xff09;进行测试&#xff0c;以验证其是否符合预期的行为和结果。单元测试通常由开发人…

【AI】《动手学-深度学习-PyTorch版》笔记(五):线性代数

AI学习目录汇总 1、标量 1.1 介绍 标量就是我们常见的单个数字(包括整数、小数等等),可以使用只有一个元素的张量表示 1.2 表示方法 用小写字母表示,如:x、y、z 1.3 程序示例 import torchx = torch.tensor(8.0) y = torch.tensor(3.0)x + y

[SQL挖掘机] - 窗口函数 - row_number

介绍: row_number() 是一种常用的窗口函数&#xff0c;它为结果集中的每一行分配一个唯一的数字。这个数字的分配基于指定的排序顺序&#xff0c;并且不会跳过相同的排名。 用法: row_number() 函数的语法如下&#xff1a; row_number() over ([partition by 列名1, 列名2,…

pgsql 查看某个表建立了那些索引sql

执行以下sql&#xff1a; SELECTns.nspname as schema_name,tab.relname as table_name,cls.relname as index_name,am.amname as index_type,idx.indisprimary as is_primary,idx.indisunique as is_unique FROMpg_index idx INNER JOIN pg_class cls ON cls.oididx.indexrel…