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

逐步学习Go-sync.Mutex(详解与实战)

概述

Go中提供了互斥锁:sync.Mutex。sync.Mutex提供了以下方法:


type Mutex
// 加锁。如果已经有goroutine持有了锁,那么就阻塞等待直到持有锁
func (m *Mutex) Lock()// 尝试加锁。如果加锁成功就返回true,否则返回失败
func (m *Mutex) TryLock() bool// 解锁。Lock和TryLock都使用Unlock来解锁。
// 如果Mutex没有调用Lock直接调用Unlock会panic。我们有UT来测试这个场景。
func (m *Mutex) Unlock()

COPY

使用锁的最佳实践:加锁之后立即调用解锁,示例如下:


mux := sync.Mutex{}mux.Lock()
defer mux.Unlock()
//接下来的所业务逻辑

COPY

公平性

sync.Mutex的公平性在实现源码里面有详细的说明。Mutex共有两种操作模式:

  1. 普通
  2. 饥饿

普通操作模式

在普通操作模式下,goroutine在没有获取到锁时会加入等待队列,队列是先进先出(FIFO order),但是如果其他goroutine释放了锁,在队列中等待锁的gorouine会和新到达的goroutine竞争锁的拥有权。一般来说新到达的goroutine会获胜因为新到达的goroutine正在CPU上运行,所以等待队列中的goroutine很可能会竞争失败。但是在竞争失败后会将唤醒的goroutine放到队列前面。如果等待的goroutine尝试获取锁的时间超过了1毫秒,那么Mutex切换到饥饿模式。

总结: 普通操作模式下,新的goutines可能会插队(抢占),这样提高了新任务的处理效率,但是可能导致队列中的goroutine一直等不到锁。为了防止等待的goroutine一直等不到锁而饿死,Mutex会被切换到饥饿操作模式。

饥饿操作模式

饥饿操作模式下,新的goroutines达到后不会抢占锁而是排队等待,排在队尾。

其他goroutines释放了锁以后队头的goroutines会获取锁。在等待队列中的最后一个goroutine获取到锁以后会将Mutex的操作模式切换到普通操作模式。

总结:在饥饿模式下,解锁的goroutine会直接将锁的所有权交给等待队列中的第一个goroutine,从而保证等待者能够及时获取到锁,防止饥饿现象。

对比

对比项普通模式饥饿模式
锁的所有权转移等待者和新到来的goroutines向互斥锁竞争解锁的goroutine直接将所有权交给队列中的第一个等待者
性能高,一个goroutine可以连续多次获得互斥锁,即使有等待者被阻塞稍低,因为每次解锁后,都会直接将所有权传递给等待队列中的下一个goroutine
公平性较差,新到来的goroutine更有可能获得锁高,等待时间较长的goroutine会被优先考虑
防止饥饿现象无明显机制有利于阻止饥饿现象,因为锁的所有权直接从当前的持有者转交给等待队列中的下一个goroutine
转换触发机制如果一个等待者尝试获取锁超过1ms都失败了,就会切换到饥饿模式如果一个goroutine得到锁后,看到它是队列中的最后一个等待者,或者它等待的时间小于1ms,就会切换回普通模式

普通模式和饥饿模式是性能和公平性的一个权衡。

sync.Mutex也有锁升级

熟悉Java的小伙伴们可能知道Java中synchronized会锁升级:无锁->偏向锁->轻量级锁->重量级锁。
ReentrantLock默认就是非公平锁:新到来的线程也会抢占一下锁,不行再排队。

默认情况下,Go语言的 sync.Mutex 是处于普通模式(类似于轻量级锁),其中主要使用的是自旋等待的方式(我个人测试是自旋4次),并且当锁被释放时,新到来的goroutine和等待队列中的goroutine并没有明确的优先级,任何一个goroutine都有可能获取到锁。

当一个goroutine在等待锁超过一定时间(默认为1ms)后,会将互斥锁设置为饥饿模式(类似于重量级锁)。在这个模式下,对锁的竞争会变得更加有序,锁会直接从当前的持有者传递给等待队列中的下一个goroutine,这就避免了新到来的goroutine可能"插队"成功的情况,降低了等待队列中的goroutine的饥饿可能。此模式下锁的获取由原来的可能的"插队"成功,变成了公平的FIFO顺序,意思是系统会保证等待时间最长的goroutine能优先获取到锁。饥饿模式虽然更公平但是会带来更多的上下文切换开销。

我们可以以锁升级的方式来理解普通模式到饥饿模式再到普通模式切换过程。

使用测试

其实测试也没太多好测的,因为使用场景比较简单。测试用例覆盖了以下场景:

  1. 测试互斥锁在未锁定状态时能否成功锁定。
  2. 测试当一个协程已经锁定互斥锁时,其他协程尝试锁定是否会被阻塞。
  3. 测试在没有其他互斥锁的情况下,尝试使用 TryLock 方法是否能成功锁定互斥锁。
  4. 测试当已有协程锁定互斥锁时,尝试使用 TryLock 方法是否不能成功锁定互斥锁。
  5. 测试在多个协程并发情况下,使用互斥锁来保护自增操作是否线程安全。
  6. 测试在未锁定互斥锁的情况下进行解锁操作是否会报错。
  7. 测试当一协程长时间持有锁时,其他协程尝试获取互斥锁是否会发生饥饿现象。

测试代码


import ("runtime""sync""testing""time""github.com/stretchr/testify/assert"
)// TestMutex_ShouldLock_WhenNotLockedBefore 测试互斥锁在未锁定状态时能否成功锁定。
// 参数 t 用于测试上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldLock_WhenNotLockedBefore(t *testing.T) {// 创建一个互斥锁mux := sync.Mutex{}// 尝试锁定互斥锁mux.Lock()// 标记锁定是否成功isSuccess := true// 确保在函数退出时解锁,避免死锁defer mux.Unlock()// 验证锁定是否成功assert.True(t, isSuccess)
}// TestMutex_ShouldBlock_WhenUsingLockAndOneRoutineHasLocked 测试当一个协程已经锁定互斥锁时,
// 其他协程尝试锁定是否会被阻塞。
// 参数 t 用于测试上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldBlock_WhenUsingLockAndOneRoutineHasLocked(t *testing.T) {// 创建一个互斥锁mux := sync.Mutex{}// 创建一个等待组,用于同步协程wg := sync.WaitGroup{}// 添加一个计数,表示要等待的一个协程wg.Add(1)// 启动一个协程去锁定互斥锁go func() {// 表示协程启动完成wg.Done()// 尝试锁定互斥锁mux.Lock()// 确保在函数退出时解锁defer mux.Unlock()// 持锁一段时间,模拟锁定状态time.Sleep(5 * time.Second)}()// 等待协程启动完成wg.Wait()println("go routine started")// 主协程尝试锁定互斥锁,应被阻塞mux.Lock()// 标记锁定是否成功isSuccess := true// 确保解锁defer mux.Unlock()// 验证主协程是否成功锁定assert.True(t, isSuccess)
}// TestMutex_ShouldLocked_WhenTryLockAndNoOtherLockers 测试在没有其他锁定的情况下,
// 尝试使用 TryLock 方法是否能成功锁定互斥锁。
// 参数 t 用于测试上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldLocked_WhenTryLockAndNoOtherLockers(t *testing.T) {// 创建一个互斥锁mux := sync.Mutex{}// 尝试立即锁定互斥锁isSuccess := mux.TryLock()// 确保解锁defer mux.Unlock()// 验证是否成功锁定assert.True(t, isSuccess)
}// TestMutex_ShouldNotLocked_WhenTryLockAndOtherLockers 测试当已有协程锁定互斥锁时,
// 尝试使用 TryLock 方法是否不能成功锁定互斥锁。
// 参数 t 用于测试上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldNotLocked_WhenTryLockAndOtherLockers(t *testing.T) {// 创建一个互斥锁mux := sync.Mutex{}// 创建一个等待组,用于同步协程wg := sync.WaitGroup{}// 添加一个计数,表示要等待的一个协程wg.Add(1)// 启动一个协程去锁定互斥锁go func() {// 表示协程启动完成//这里先调用Done是为了不继续阻塞wg.Wait()让wg.Wait()之后的mutex.Lock可以继续执行wg.Done()// 尝试锁定互斥锁mux.Lock()// 确保在函数退出时解锁defer mux.Unlock()// 持锁一段时间,模拟锁定状态time.Sleep(5 * time.Second)}()// 等待协程启动完成wg.Wait()// 尝试立即锁定互斥锁isSuccess := mux.TryLock()// 确保解锁defer mux.Unlock()// 验证是否未能成功锁定assert.False(t, isSuccess)
}// TestMutex_ShouldIncrementCounterSuccess_WhenUseMultipleGoroutineAndAddCounterInLock 测试在多个协程并发情况下,使用Mutex锁定来增加计数器是否成功
// 参数:
// - t *testing.T: 测试环境的句柄,用于报告测试失败和日志记录
// 返回值: 无
func TestMutex_ShouldIncrementCounterSuccess_WhenUseMultipleGoroutineAndAddCounterInLock(t *testing.T) {// 初始化互斥锁、等待组和计数器mux := sync.Mutex{}wg := sync.WaitGroup{}counter := 0// 设置运行时的最大协程数为10,以模拟并发环境runtime.GOMAXPROCS(10)// 添加100个协程来并发执行增加计数器的操作wg.Add(100)for i := 0; i < 100; i++ {go func() {// 在协程结束时释放等待组defer wg.Done()// 加锁以确保对计数器的操作是互斥的mux.Lock()defer mux.Unlock()// 增加计数器counter++}()}// 等待所有协程完成wg.Wait()// 验证计数器是否被正确地增加了100次assert.Equal(t, 100, counter)
}// TestMutex_ShouldPanic_WhenUnlockWithoutLock 测试当互斥锁没有被锁定时,尝试解锁是否会引发Panic。
// 参数 t *testing.T 用于测试的上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldPanic_WhenUnlockWithoutLock(t *testing.T) {// 创建一个 sync.Mutex 实例。mux := sync.Mutex{}// 使用 assert 包的 Panics 函数来断言是否会引发Panic。assert.Panics(t, func() {// 尝试解锁一个没有被锁定的互斥锁。mux.Unlock()})
}// TestMutex_ShouldStarvation_WhenOneRoutineHoldLockedForLongTime 测试当一个协程长时间持有互斥锁时,
// 其他协程是否会出现饥饿现象。参数 t *testing.T 用于测试的上下文,提供测试控制和日志记录功能。
func TestMutex_ShouldStarvation_WhenOneRoutineHoldLockedForLongTime(t *testing.T) {// 定义一个 sync.Mutex 实例用于测试互斥锁饥饿问题。var mu sync.Mutex// 用于同步协程开始的 waitgroup。var start sync.WaitGroup// 用于同步协程结束的 waitgroup。var done sync.WaitGroupstart.Add(1) // 准备启动一个协程。done.Add(1)  // 等待一个协程完成。go func() {  // 启动一个协程长时间持有锁。start.Done() // 表示协程已启动并准备好。mu.Lock()    // 获取锁并长时间持有。time.Sleep(1000 * time.Second)mu.Unlock() // 最终释放锁。done.Done() // 表示协程已完成。}()start.Wait()                 // 等待协程开始并持有锁。time.Sleep(time.Millisecond) // 稍微延时以确保锁被持有。start.Add(1) // 准备启动另一个协程。done.Add(1)  // 等待另一个协程完成。go func() {  // 启动另一个协程尝试获取锁,以测试是否出现饥饿。start.Done()                             // 表示协程已启动并准备好。mu.Lock()                                // 尝试获取锁。t.Log("Starving goroutine got the lock") // 如果获取到锁,则记录日志。mu.Unlock()                              // 最终释放锁。done.Done()                              // 表示协程已完成。}()start.Wait()                 // 等待第二个协程开始。time.Sleep(time.Millisecond) // 稍微延时以确保尝试获取锁的协程已运行。mu.Lock()                            // 主协程尝试获取锁,以进一步测试饥饿情况。t.Log("Main goroutine got the lock") // 如果获取到锁,则记录日志。mu.Unlock()                          // 释放主协程持有的锁。done.Wait() // 等待所有协程完成,确保测试完整执行。
}

相关文章:

逐步学习Go-sync.Mutex(详解与实战)

概述 Go中提供了互斥锁&#xff1a;sync.Mutex。sync.Mutex提供了以下方法&#xff1a; type Mutex // 加锁。如果已经有goroutine持有了锁&#xff0c;那么就阻塞等待直到持有锁 func (m *Mutex) Lock()// 尝试加锁。如果加锁成功就返回true&#xff0c;否则返回失败 func (m…...

每日三道面试题之 Java并发编程 (一)

1.为什么要使用并发编程 并发编程是一种允许多个操作同时进行的编程技术&#xff0c;这种技术在现代软件开发中非常重要&#xff0c;原因如下&#xff1a; 充分利用多核处理器&#xff1a;现代计算机通常都拥有多核处理器&#xff0c;通过并发编程&#xff0c;可以让每个核心独…...

车身稳定控制系统原理是什么?

车身稳定控制系统&#xff08;Electronic Stability Control&#xff0c;ESC&#xff09;是一种先进的车辆动态控制系统&#xff0c;其主要原理是通过传感器监测车辆的各项状态&#xff0c;包括车速、转向角度、侧倾角等&#xff0c;然后通过电子控制单元&#xff08;ECU&#…...

vue3前端加载动画 lottie-web 的简单使用案例

什么是 Lottie Lottie 是 Airbnb 发布的一款开源动画库&#xff0c;它适用于 Android、iOS、Web 和 Windows 的库。 它提供了一套从设计师使用 AE&#xff08;Adobe After Effects&#xff09;到各端开发者实现动画的工具流。 UED 提供动画 json 文件即可&#xff0c; 开发者就…...

基于java+springboot+vue实现的健身房管理系统(文末源码+Lw)23-223

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装健身房管理系统软件来发挥其高效地信息处理的作用&#xf…...

10款白嫖党必备的ai写作神器,你都知道吗? #媒体#人工智能#其他

从事自媒体运营光靠自己手动操作效率是非常低的&#xff0c;想要提高运营效率就必须要学会合理的使用一些辅助工具。下面小编就跟大家分享一些自媒体常用的辅助工具&#xff0c;觉得有用的朋友可以收藏分享。 1.飞鸟写作 这是一个微信公众号 面向专业写作领域的ai写作工具&am…...

Docker工作流

1.工作流 开发应用编写Dockerfile构建Docker镜像运行Docker容器测试应用发布镜像到Hub迭代更新镜像 2.开发应用 首先你需要创建一个应用&#xff0c;这个应用可以是后端应用或者前端应用&#xff0c;任何语言都可以。 比如&#xff1a;我使用IDEA 创建一个Java后端应用&…...

深入浅出 -- 系统架构之分布式集群的分类

一、单点故障问题 集群&#xff0c;相信诸位对这个概念并不陌生&#xff0c;集群已成为现时代中&#xff0c;保证服务高可用不可或缺的一种手段。 回想起初集中式部署的单体应用&#xff0c;因为只有一个节点&#xff0c;因此当该节点出现任意类型的故障&#xff08;网络、硬件…...

Docker之镜像与容器的相关操作

目录 一、Docker镜像 搜索镜像 下载镜像 查看宿主机上的镜像 删除镜像 二、Docker容器 创建容器 查看容器 启停容器 删除容器 进入容器 创建/启动/进入容器 退出容器 查看容器内部信息 一、Docker镜像 Docker 运行容器前需要本地存在对应的镜像&#xff0c; 如…...

中科驭数超低时延网络解决方案入选2023年度金融信创优秀解决方案

近日&#xff0c;由中国人民银行领导、中国金融电子化集团有限公司牵头组建的金融信创生态实验室发布「2023年度第三期金融信创优秀解决方案」&#xff0c;中科驭数超低时延网络解决方案从众多方案中脱颖而出&#xff0c;成功入选&#xff0c;代表了该方案的技术创新和金融实践…...

应用方案 | DCDC电源管理芯片MC34063A

MC34063A 为一单片 DC-DC 变换集成电路&#xff0c;内含温度补偿的参考电压源&#xff08;1.25V&#xff09;、比较器、能有效限制电流及控制工作周期的振荡器&#xff0c;驱动器及大电流输出开关管等。外配少量元件&#xff0c;就能组成升压、降压及电压反转型 DC-DC 变换器。…...

【个人使用推荐】联机不卡顿 小白一键部署 大厂云服务器选购指南 16G低至26 幻兽帕鲁最大更新来袭

更新日期&#xff1a;4月8日&#xff08;半年档 价格回调&#xff0c;京东云采购季持续进行&#xff09; 本文纯原创&#xff0c;侵权必究 《最新对比表》已更新在文章头部—腾讯云文档&#xff0c;文章具有时效性&#xff0c;请以腾讯文档为准&#xff01; 【腾讯文档实时更…...

57 npm run build 和 npm run serve 的差异

前言 npm run serve 和 npm run build 的差异 这里主要是从 vue-cli 的流程 来看一下 我们经常用到的这两个命令, 他到传递给 webpack 打包的时候, 的一个具体的差异, 大致是配置了那些东西? 经过了那些流程 ? vue-cli 的 vue-plugin 的加载 内置的 plugin 列表如下, 依次…...

原生小程序开发性能优化指南

性能优化指南 1.骨架屏 业务可以在数据加载完成之前用骨架屏幕来占位&#xff0c;提升体验。 2.包大小优化 减小包中静态资源&#xff0c;例如图片文件&#xff0c;可将图片进行压缩降低文件体积。无用文件、函数、样式剔除。除了部分用于容错的图片必须放在代码包&#xf…...

「51媒体网」邀请媒体采访报道对企业宣传有何意义?

传媒如春雨&#xff0c;润物细无声的&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 邀请媒体采访报道对企业宣传具有多重意义&#xff1a; 提升品牌知名度和曝光度&#xff1a;媒体是信息传播的重要渠道&#xff0c;通过媒体的报道&#xff0c;企业及其活动、产品能够迅…...

用动态IP采集数据总是掉线是为什么?该怎么解决?

动态IP可以说是做爬虫、采集数据、搜集热门商品信息中必备的代理工具&#xff0c;但在爬虫的使用中&#xff0c;总是会遇到动态IP掉线的情况&#xff0c;从而影响使用效率&#xff0c;本文将探讨动态IP代理掉线的几种常见原因&#xff0c;并提供解决方法&#xff0c;以帮助大家…...

MySQL操作DDL

目录 1.概述 2.数据库的增删改查 3.表的增删改查 3.1.创建和查看表结构 3.2.修改表 3.3.查看所有的表 3.4.删除表 4.用户 5.DDL在实际应用场景中的作用 5.1.数据库设计 5.2.数据库维护 ​​​​​​​5.3.数据库迁移或重置 ​​​​​​​5.4.优化性能 ​​​​​…...

程序员如何搞副业

目录 1.概述 2.个人项目开发 3.在线教育和培训 4.技术博客和内容创作 1.概述 程序员通过副业实现个人价值最大化和增加收入的途径多种多样&#xff0c;以下是一些方法&#xff1a; 自由职业: 程序员可以在业余时间提供自由职业服务。包括为客户开发软件、网站或应用程序、…...

【嵌入式开发 Linux 常用命令系列 4.3 -- git add 不 add untracked file】

请阅读【嵌入式开发学习必备专栏 】 文章目录 git add 不add untracked file git add 不add untracked file 如果你想要Git在执行git add .时不添加未跟踪的文件&#xff08;untracked files&#xff09;&#xff0c;你可以使用以下命令&#xff1a; git add -u这个命令只会加…...

git 常用命令和使用方法

作者简介&#xff1a; 一个平凡而乐于分享的小比特&#xff0c;中南民族大学通信工程专业研究生在读&#xff0c;研究方向无线联邦学习 擅长领域&#xff1a;驱动开发&#xff0c;嵌入式软件开发&#xff0c;BSP开发 作者主页&#xff1a;一个平凡而乐于分享的小比特的个人主页…...

【网络】每天掌握一个Linux命令 - iftop

在Linux系统中&#xff0c;iftop是网络管理的得力助手&#xff0c;能实时监控网络流量、连接情况等&#xff0c;帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

DockerHub与私有镜像仓库在容器化中的应用与管理

哈喽&#xff0c;大家好&#xff0c;我是左手python&#xff01; Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库&#xff0c;用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...

JVM垃圾回收机制全解析

Java虚拟机&#xff08;JVM&#xff09;中的垃圾收集器&#xff08;Garbage Collector&#xff0c;简称GC&#xff09;是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象&#xff0c;从而释放内存空间&#xff0c;避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

第25节 Node.js 断言测试

Node.js的assert模块主要用于编写程序的单元测试时使用&#xff0c;通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试&#xff0c;通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

数据库分批入库

今天在工作中&#xff0c;遇到一个问题&#xff0c;就是分批查询的时候&#xff0c;由于批次过大导致出现了一些问题&#xff0c;一下是问题描述和解决方案&#xff1a; 示例&#xff1a; // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

网络编程(UDP编程)

思维导图 UDP基础编程&#xff08;单播&#xff09; 1.流程图 服务器&#xff1a;短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

虚拟电厂发展三大趋势:市场化、技术主导、车网互联

市场化&#xff1a;从政策驱动到多元盈利 政策全面赋能 2025年4月&#xff0c;国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》&#xff0c;首次明确虚拟电厂为“独立市场主体”&#xff0c;提出硬性目标&#xff1a;2027年全国调节能力≥2000万千瓦&#xff0…...

【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案

目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后&#xff0c;迭代器会失效&#xff0c;因为顺序迭代器在内存中是连续存储的&#xff0c;元素删除后&#xff0c;后续元素会前移。 但一些场景中&#xff0c;我们又需要在执行删除操作…...