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

深入Golang之Mutex

深入Golang之Mutex

在这里插入图片描述

基本使用方法

可以限制临界区只能同时由一个线程持有。

  • 直接在流程结构中使用 lockunlock
  • 嵌入到结构中,然后通过结构体的 mutex 属性 调用 lockunlock
  • 嵌入到结构体中,但是是直接在需要锁定的资源方法中使用,让外界无需关注资源锁定

在进行资源锁定的过程中,很容易出现 data race,这时候我们可以使用 race detector ,融入到 持续集成 中,以减少代码的 Bug

看实现

在这里插入图片描述

初版互斥锁

设立持有锁的标识 flagsema 信号量来控制互斥,实际上是利用 CAS 指令完成原子计算。

  • 字段 key:是一个 flag,用来标识这个排外锁是否被某个 goroutine 所持有,如果 key 大于等于 1,说明这个排外锁已经被持有; key 不仅仅标识了锁是否被 goroutine 所持有,还记录了当前持有和等待获取锁
    goroutine 的数量
  • 字段 sema:是个信号量变量,用来控制等待 goroutine 的阻塞休眠和唤醒。

Unlock 方法可以被任意的 goroutine 调用释放锁,即使是没持有这个互斥锁的 goroutine,也可以进行这个操作。这是因为,Mutex 本身并没有包含持有这把锁的 goroutine 的信息,所以,Unlock 也不会对此进行检查。Mutex 的这个设计一直保持至今。

由于上面这个原因,就有可能出现 if 判断中释放其他 goroutine,释放锁的 goroutine 不必是锁的持有者

func lockTest()
{lock()var countif count {unlock()	}// 此处就可能出现 goroutine 释放其他的锁unlock()
}

四种常见使用错误

Lock/Unlock 不是成对出现的,漏写、意外删除

Copy已使用的 Mutex

type Counter struct { sync.MutexCount int
}
func main() { var c Counterc.Lock()defer c.Unlock()c.Count++foo(c) // 复制锁
}
// 这里Counter的参数是通过复制的方式传入的
func foo(c Counter) { c.Lock() defer c.Unlock()fmt.Println("in foo")
}

为什么它不能被复制?

原因在于 Mutex 是一个有状态的对象,它的 state 字段记录这个锁的状态。如果你要复制一个已经加锁的 Mutex 给一个新的变量,那么新的刚初始化的变量居然被加锁了,这显然不符合预期

重入

  • 可重入锁概念解释

当一个线程获取锁时,如果没有其他线程拥有这个锁,那么这个线程就成功获取了这个锁,之后,如果其他线程再去请求这个锁,就会处于阻塞状态。如果拥有这把锁的线程再请求这把锁的话,不会阻塞,而是成功返回,所以叫可重入锁。

  • Mutex 不是可重入锁

想想也不奇怪,因为 Mutex 的实现中没有记录哪个 goroutine 拥有这把锁。理论上,任何 goroutine 都可以随意地 Unlock 这把锁,所以没办法计算重入条件

func foo(l sync.Locker) {fmt.Println("in foo")l.Lock()bar(l)l.Unlock()
}
// 这就是可重入锁
func bar(l sync.Locker) {l.Lock()fmt.Println("in bar")l.Unlock()
}
func main() {l := &sync.Mutex{}foo(l)
}

自己实现可重入锁

  • 通过 goroutine id

// RecursiveMutex 包装一个Mutex,实现可重入
type RecursiveMutex struct {sync.Mutexowner     int64 // 当前持有锁的goroutine idrecursion int32 // 这个goroutine 重入的次数
}func (m *RecursiveMutex) Lock() {gid := goid.Get() // 如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入if atomic.LoadInt64(&m.owner) == gid {m.recursion++return}m.Mutex.Lock() // 获得锁的goroutine第一次调用,记录下它的goroutine id,调用次数加1atomic.StoreInt64(&m.owner, gid)m.recursion = 1
}func (m *RecursiveMutex) Unlock() {gid := goid.Get() // 非持有锁的goroutine尝试释放锁,错误的使用if atomic.LoadInt64(&m.owner) != gid {panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))} // 调用次数减1m.recursion--if m.recursion != 0 { // 如果这个goroutine还没有完全释放,则直接返回return} // 此goroutine最后一次调用,需要释放锁atomic.StoreInt64(&m.owner, -1)m.Mutex.Unlock()
}

有一点,要注意,尽管拥有者可以多次调用 Lock,但是也必须调用相同次数的 Unlock,这样才能把锁释放掉。这是一个合理的设计,可以保证 LockUnlock 一一对应。

  • 方案二:token

这个与 goroutine id 差不多, goroutine id 既然没有暴露出来,说明设计方不希望使用这个,而这只是可重入锁的一个标识,我们可以自定义这个标识,由协程自己提供,在调用 lockunlock 中,自己传入一个生成的 token 即可,逻辑是一样的

死锁

  • 互斥: 排他性资源
  • 环路等待: 形成环路
  • 持有和等待: 持有还去和其他资源竞争
  • 不可剥夺: 资源只能由持有它的 goroutine 释放

打破以上条件其中一个或者几个即可解除死锁

扩展 Mutex

  • 实现 TryLock
  • 获取等待者的数量等指标
  • 使用 Mutex 实现一个线程安全的队列

读写锁的实现原理及避坑指南

标准库中的 RWMutex 是一个 reader/writer 互斥锁。RWMutex 在某一时刻只能由任意数量的 reader 持有,或者是只被单个的 writer 持有。
他是基于 Mutex 的。如果你遇到可以明确区分 readerwriter goroutine 的场景,且有大量的并发读、少量的并发写,并且有强烈的性能需求,你就可以考虑使用读写锁 RWMutex 替换 Mutex

读写锁的实现方式

  • Read-preferring:读优先的设计可以提供很高的并发性,但是,在竞争激烈的情况下可能会导致写饥饿。这是因为,如果有大量的读,这种设计会导致只有所有的读都释放了锁之后,写才可能获取到锁。
  • Write-preferring:写优先的设计意味着,如果已经有一个 writer 在等待请求锁的话,它会阻止新来的请求锁的 reader 获取到锁,所以优先保障 writer。当然,如果有一些 reader 已经请求了锁的话,新请求的 writer 也会等待已经存在的 reader 都释放锁之后才能获取。所以,写优先级设计中的优先权是针对新来的请求而言的。这种设计主要避免了 writer 的饥饿问题。
  • 不指定优先级:这种设计比较简单,不区分 reader 和 writer 优先级,某些场景下这种不指定优先级的设计反而更有效,因为第一类优先级会导致写饥饿,第二类优先级可能会导致读饥饿,这种不指定优先级的访问不再区分读写,大家都是同一个优先级,解决了饥饿的问题。

Go 标准库中的 RWMutex 设计是 Write-preferring 方案。一个正在阻塞的 Lock 调用会排除新的 reader 请求到锁。

RWMutex 的 3 个踩坑点

  • 不可复制
  • 重入导致死锁
  • 释放未加锁的 RWMutex

我们知道,有活跃 reader 的时候,writer 会等待,如果我们在 reader 的读操作时调用 writer 的写操作(它会调用 Lock 方法),那么,这个 readerwriter 就会形成互相依赖的死锁状态。Reader 想等待 writer 完成后再释放锁,而 writer 需要这个 reader 释放锁之后,才能不阻塞地继续执行。这是一个读写锁常见的死锁场景。

第三种死锁的场景更加隐蔽。
当一个 writer 请求锁的时候,如果已经有一些活跃的 reader,它会等待这些活跃的 reader 完成,才有可能获取到锁,但是,如果之后活跃的 reader 再依赖新的 reader 的话,这些新的 reader 就会等待 writer 释放锁之后才能继续执行,这就形成了一个环形依赖: writer 依赖活跃的 reader -> 活跃的 reader 依赖新来的 reader -> 新来的 reader 依赖 writer。

相关文章:

深入Golang之Mutex

深入Golang之Mutex 基本使用方法 可以限制临界区只能同时由一个线程持有。 直接在流程结构中使用 lock、unlock嵌入到结构中,然后通过结构体的 mutex 属性 调用 lock、unlock嵌入到结构体中,但是是直接在需要锁定的资源方法中使用,让外界无…...

高并发内存池项目(C++实战项目)

项目介绍 项目来源 本项目实现了一个高并发内存池,参考了Google的开源项目tcmalloc实现的简易版;其功能就是实现高效的多线程内存管理。由功能可知,高并发指的是高效的多线程,而内存池则是实现内存管理的。 tcmalloc源码 ▶项…...

G. The Morning Star - 思维

分析: 直接暴力就会tle,不知道怎么下手,可以统计八个方向一条线上的所有坐标,这些坐标一定可以放在一起满足,分析都有哪些线,当横坐标相同时会有竖着的一条线都可以,也就是x c,当纵…...

应急物资管理系统|智物资DW-S300提升应急响应能力

项目背景 智慧应急物资管理系统(智装备DW-S300)是一套成熟系统,依托互3D技术、云计算、大数据、RFID技术、数据库技术、AI、视频分析技术对RFID智能仓库进行统一管理、分析的信息化、智能化、规范化的系统。 本项目采用东识智慧应急物资管理…...

AI人员打架识别算法

AI打架识别算法通过yolov8网络模型算法框架,AI打架识别算法识别校园打架斗殴行为,发现立即打架斗殴行为算法会立即抓拍告警推送打架事件信息。目标检测架构分为两种,一种是two-stage,一种是one-stage,区别就在于 two-s…...

NSS [NUSTCTF 2022 新生赛]Ezjava1

NSS [NUSTCTF 2022 新生赛]Ezjava1 题目描述:你能获取flag{1}吗 开题,一眼java web中的index.jsp。 默认index.jsp中的body内容是$END$ 附件jar包导入IDEA,会自动反编译。看看源码。 附件结构大致如此。主要看classes.com.joe1sn中的代码就…...

【Go 基础篇】探索Go语言中Map的神奇操作

嗨,Go语言的学习者们!在编程世界中,Map是一个强大而又有趣的工具,它可以帮助我们高效地存储和操作键值对数据。Map就像是一本字典,可以让我们根据关键字(键)快速找到对应的信息(值&a…...

第6篇:ESP32连接无源喇叭播放音乐《涛声依旧》

第1篇:Arduino与ESP32开发板的安装方法 第2篇:ESP32 helloword第一个程序示范点亮板载LED 第3篇:vscode搭建esp32 arduino开发环境 第4篇:vscodeplatformio搭建esp32 arduino开发环境 第5篇:doit_esp32_devkit_v1使用pmw呼吸灯实验 D5连接喇叭正极,GND连接喇叭负…...

Linux用户组管理学习

1.创建一个用户组...

【知识分享】C语言应用-易错篇

一、C语言简介 C语言结构简洁,具有高效性和可移植性,因此被广泛应用。但究其历史的标准定义,C语言为了兼容性在使用便利性作出很大牺牲。在《C陷阱与缺陷》一书中,整理出大部分应用过程中容易出错的点,本文为《C陷阱与…...

六、Json 数据的交互处理

文章目录 一、JSON 数据的交互处理1、为什么要使用 JSON2、JSON 和 JavaScript 之间的关系3、前端操作 JSON3.1 JavaScript 对象与 JSON 字符串之间的相互转换 4、JAVA 操作 JSON4.1 Json 的解析工具(Gson、FastJson、Jackson)4.2 ResponseBody 注解、Re…...

企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC

文章目录 企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC1. 企业微信cgi-bin/gateway/agentinfo接口简介2.漏洞描述3.影响版本4.fofa查询语句5.漏洞复现6.POC&EXP7.整改意见8.往期回顾 企业微信cgi-bin/gateway/agentinfo接口存在未授权访问漏洞 附POC 免…...

【数据结构与算法 模版】高频题刷题模版

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【】,使用【】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为:目标公…...

西门子840DSL 840DPoweLine 刀具数据读取

...

EMQ X支持哪些认证方式?

EMQ X 中的认证指的是当一个客户端连接到 EMQ X 的时候,通过服务器端的配置来控制客户端连接服务器的权限。 EMQ X 的认证支持包括两个层面: MQTT 协议本身在 CONNECT 报文中指定用户名和密码,EMQ X 以插件形式支持基于 Username、 ClientI…...

java八股文面试[JVM]——JVM内存结构2

知识来源: 【2023年面试】JVM内存模型如何分配的_哔哩哔哩_bilibili...

《C和指针》笔记14: 作用域和存储类型总结(例子说明)

文章目录 题目答案解释总结 本文是作用域和存储类型的总结,以一个例子来说明,如果不看解释可以很直接地回答每一条语句的作用域和存储类型,那么说明已经很熟练地掌握这个知识点了。 关于作用域和存储类型可以参考我前面的博客: …...

Linux之系统操作参数详解

Linux之系统操作参数详解 date //显示当前日期 日期格式化 %Y year年 %y 年份(以00-99来表示) %j 该年中的第几天 %m month月 (01…12) %w 该周的天数,0代表周日,1代表周一 %D 日期(含年月日) %d day of month (e.g., 01) %T 时间(含时分秒&#xff0…...

datax 使用

环境准备 List itemLinuxJDK(1.8以上,推荐1.8)Python(2或3都可以)Apache Maven 3.x (Compile DataX) 下载 wget https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202308/datax.tar.gz建立datax 用户 useradd datax ; echo "datax" | passwd -…...

【C/C++】#define宏替换高级用法

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…...

Android 之 传感器专题 (4) —— 其他传感器了解

本节引言&#xff1a; 在上一节的结尾说了&#xff0c;传感器部分因为笔者没怎么玩过&#xff0c;本节就简单的把剩下的几个常用的 传感器介绍一遍&#xff0c;当作科普&#xff0c;以后用到再慢慢研究~ 1.磁场传感器(Magnetic field sensor) 作用&#xff1a;该传感器主要用…...

【高级搜索】双向广搜,A*,IDDFS,IDA *算法总结 (terse版)

一、双向广搜 ​ 双向广搜就是从起点和终点同时往中间搜的一个算法。 ​ 注意事项&#xff1a; ​ 在搜索过程中&#xff0c;同一层次下的顺序应该为&#xff1a;搜完一边所有的当前深度的子节点&#xff0c;在搜索另一边。 ​ 队列使用 ​ &#xff08;1&#xff09;合用…...

CATIA Composer R2023安装教程

软件下载 软件&#xff1a;CATIA Composer版本&#xff1a;2023语言&#xff1a;简体中文大小&#xff1a;1.82G安装环境&#xff1a;Win11/Win10/Win8/Win7硬件要求&#xff1a;CPU2.60GHz 内存8G(或更高&#xff09;下载通道①百度网盘丨64位下载链接&#xff1a;https://pa…...

git,修改远程分支名称

获取所有远程分支 git branch -r删除远程指定分支 git push --delete origin 测试添加新页面提交新命名本地分支 git push origin 新分支本地分支和远程分支关联 git branch --set-upstream-to origin/远程分支...

信息系统项目管理师(第四版)教材精读思维导图-第六章项目管理理论

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 目录 6.1 PMBOK的发展 6.2 项目基本要素 6.3…...

[Android]JNI的基础知识

目录 1.什么是JNI 2.配置JNI开发环境NDK 3.创建Native C类型的项目 4. 了解CMakeLists.txt 文件 5.了解native-lib.cpp 文件 6.在 Android 的 MainActivity 中调用 native-lib.cpp 中实现的本地方法 1.什么是JNI JNI&#xff08;Java Native Interface&#xff09;是一…...

力扣-哈希-字母异位词分组

题目 给你一个字符串数组&#xff0c;请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。 示例 1: 输入: strs ["eat", "tea", "tan", "ate", "nat&q…...

excel 分组排序

excel中会遇到对不同分组数据进行排序&#xff0c;比如对于不同班级里的学生按照分数高低进行升序排序&#xff0c;可以采用如下公式 SUMPRODUCT((A$2:A$12A2)*(C$2:C$12>C2))1 如果需要 进行降序排序&#xff0c;将公式中的大于号替换为小于号即可...

vue三级市区联动

默认返回值格式&#xff1a;all:code、name都返回 name:只返回name code:只返回code&#xff0c;level&#xff1a;可设置显示层级 1&#xff1a; 省 2&#xff1a; 省、市 3&#xff1a; 省、市、区 v-model 默认值 可以是 name: [ "天津市", "天津市",…...

C++ 组合类

所谓组合类便是一个类中含有其他类的对象: #include <iostream> #include <string.h> using namespace std;class dog { public:dog() {}dog(const char *n, int age, const char *v) : age(age){strcpy(this->name, n);strcpy(this->var, v);}// 显示这条狗…...