深入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。

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

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

相关文章

WPF基础入门-Class2-样式

WPF基础入门 Class2&#xff1a;样式 1、内联样式&#xff1a;优先度最高 <Grid><StackPanel><!--内联样式优先度高--><Button Background"Red" Height"10" Width"100"FontSize"20"Content"SB">…

nginx 中新增url请求参数

1、nginx中新增配置&#xff1a; set $args "$args&参数名参数值"; 示例&#xff1a; set $args "$args&demo1cn_yaojin&demo2123123&myip$remote_addr"; location / {add_header Access-Control-Allow-Origin *;add_header Access-Contro…

计算机竞赛 基于卷积神经网络的乳腺癌分类 深度学习 医学图像

文章目录 1 前言2 前言3 数据集3.1 良性样本3.2 病变样本 4 开发环境5 代码实现5.1 实现流程5.2 部分代码实现5.2.1 导入库5.2.2 图像加载5.2.3 标记5.2.4 分组5.2.5 构建模型训练 6 分析指标6.1 精度&#xff0c;召回率和F1度量6.2 混淆矩阵 7 结果和结论8 最后 1 前言 &…

ssm+vue农家乐信息平台源码和论文

ssmvue农家乐信息平台源码和论文066 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 1、研究现状 国外&#xff0c;农家乐都被作为潜在的发展农村经济&#xff0c;增加农民收入的重要手段&#xff0c;让农户广…

【Qt专栏】实现单例程序,禁止程序多开的几种方式

目录 一&#xff0c;简要介绍 二&#xff0c;实现示例&#xff08;Windows&#xff09; 1.使用系统级别的互斥机制 2.通过共享内存&#xff08;进程间通信-IPC&#xff09; 3.使用命名互斥锁&#xff08;不推荐&#xff09; 4.使用文件锁 5.通过网络端口检测 一&#xf…

LLM提示词工程和提示词工程师Prompting and prompt engineering

你输入模型的文本被称为提示&#xff0c;生成文本的行为被称为推断&#xff0c;输出文本被称为完成。用于提示的文本或可用的内存的全部量被称为上下文窗口。尽管这里的示例显示模型表现良好&#xff0c;但你经常会遇到模型在第一次尝试时无法产生你想要的结果的情况。你可能需…

文件服务器实现方式汇总

hello&#xff0c;伙伴们&#xff0c;大家好&#xff0c;今天这一期shigen来给大家推荐几款可以一键实现文件浏览器的工具&#xff0c;让你轻松的实现文件服务器和内网的文件传输、预览。 基于node 本次推荐的是http-server&#xff0c; 它的githuab地址是&#xff1a;http-s…

Spring 与【MyBatis 】和【 pageHelper分页插件 】整合

目录 一、Spring整合MyBatis 1. 导入pom依赖 2. 利用mybatis逆向工程生成模型层层代码 3. 编写配置文件 4. 注解式开发 5. 编写Junit测试类 二、AOP整合pageHelper分页插件 1. 创建一个AOP切面 2. Around("execution(* *..*xxx.*xxx(..))") 表达式解析 3. 编…

Netty核心源码解析(三)--NioEventLoop

NioEventLoop介绍 NioEventLoop继承SingleThreadEventLoop,核心是一个单例线程池,可以理解为单线程,这也是Netty解决线程并发问题的最根本思路--同一个channel连接上的IO事件只由一个线程来处理,NioEventLoop中的单例线程池轮询事件队列,有新的IO事件或者用户提交的task时便执…

2023谷歌开发者大会直播大纲「初稿」

听人劝、吃饱饭,奉劝各位小伙伴,不要订阅该文所属专栏。 作者:不渴望力量的哈士奇(哈哥),十余年工作经验, 跨域学习者,从事过全栈研发、产品经理等工作,现任研发部门 CTO 。荣誉:2022年度博客之星Top4、博客专家认证、全栈领域优质创作者、新星计划导师,“星荐官共赢计…

【java】【idea2023版】Springboot模块没有.iml文件的问题

目录 方法一&#xff1a; 1、首先鼠标选中对应的对应的模块 &#xff0c;按两下Ctrl键 2、project中选择对应的模块 3、运行mvn idea:module 命令​编辑 方法二&#xff1a; 1、可以右键点击open Terminal 2、然后在打开的Terminal里输入 方法一&#xff1a; 1、首先鼠…

【Rust】Rust学习 第十八章模式用来匹配值的结构

模式是 Rust 中特殊的语法&#xff0c;它用来匹配类型中的结构&#xff0c;无论类型是简单还是复杂。结合使用模式和 match 表达式以及其他结构可以提供更多对程序控制流的支配权。模式由如下一些内容组合而成&#xff1a; 字面值解构的数组、枚举、结构体或者元组变量通配符占…

python VTK PyQt5 VTK环境搭建 创建 渲染窗口及三维模型,包含 三维模型交互;

目录 Part1. VTK 介绍 Part2. PyQt5 VTK环境搭建 安装Anaconda 自带Python Anaconda下载 安装PyQt5 安装 VTK Part3 :PyQt VTK 结合样例: Part1. VTK 介绍 VTK&#xff08;visualization toolkit&#xff09;是一个开源的免费软件系统&#xff0c;主要用于三维计算机图形…

数字孪生赋能工业制造,为制造业带来新机遇与挑战

数字孪生技术是利用模拟仿真技术将实体对象数字化的技术。它基于虚拟现实、人工智能和云计算等技术&#xff0c;能够创建与真实物体相同的数字模型&#xff0c;并通过实时监测和分析手段&#xff0c;为制造企业提供关于该物体的全面数据&#xff0c;从而优化产品开发和生产过程…

数据结构双向链表

Hello&#xff0c;好久不见&#xff0c;今天我们讲链表的双向链表&#xff0c;这是一个很厉害的链表&#xff0c;带头双向且循环&#xff0c;学了这个链表&#xff0c;你会发现顺序表的头插头删不再是一个麻烦问题&#xff0c;单链表的尾插尾删也变得简单起来了&#xff0c;那废…

jvm——内存模型

1.java内存模型 1.1 原子性 1.2 问题分析 这里与局部变量自增不同&#xff0c;局部变量调用iinc是在局部变量表槽位上进行自增。 静态变量是在操作数栈自增。 这里的主内存和工作内存时再JMM里的说法。 因为操作系统是时间片切换的多个线程轮流使用CPU. 1.3解决方法 JMM中…

C#矩阵XY排序

矩阵XY快速排序 using MyVision.Script.Method;public class MyScript : ScriptMethods {//struct MOTIONPOSXY_S{public double Pos_x;public double Pos_y;};//脚本执行该方法public bool Process(){//try{//脚本代码写在下方 List<double> PointX GetDoubleList(&qu…

Tensor-动手学深度学习-李沐_笔记

介绍 Tensor&#xff0c;又称"张量"&#xff0c;其实就是n维度数组。不同维度的Tensor示意图如下&#xff1a; 关于Tensor.reshape reshape函数可以处理总元素个数相同的任何新形状&#xff0c;【3&#xff0c;2&#xff0c;5】->【3&#xff0c;10】->【5&a…

[系统] 电脑突然变卡 / 电脑突然** / 各种突发情况解决思路

今天来公司办公&#xff0c;开机之后发现电脑出现各种问题&#xff0c;死机、卡顿、点什么都加载&#xff0c;甚至开一个文件夹要1分钟才能打开&#xff0c;花了2个小时才解决&#xff0c;走了很多弯路&#xff0c;其实早点想通&#xff0c;5分钟就能解决问题&#xff0c;所以打…

本地部署 Stable Diffusion(Mac 系统)

在 Mac 系统本地部署 Stable Diffusion 与在 Windows 系统下本地部署的方法本质上是差不多的。 一、安装 Homebrew Homebrew 是一个流行的 macOS &#xff08;或 Linux&#xff09;软件包管理器&#xff0c;用于自动下载、编译和安装各种命令行工具和应用程序。有关说明请访问官…