逐步学习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共有两种操作模式:
- 普通
- 饥饿
普通操作模式
在普通操作模式下,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能优先获取到锁。饥饿模式虽然更公平但是会带来更多的上下文切换开销。
我们可以以锁升级的方式来理解普通模式到饥饿模式再到普通模式切换过程。
使用测试
其实测试也没太多好测的,因为使用场景比较简单。测试用例覆盖了以下场景:
- 测试互斥锁在未锁定状态时能否成功锁定。
- 测试当一个协程已经锁定互斥锁时,其他协程尝试锁定是否会被阻塞。
- 测试在没有其他互斥锁的情况下,尝试使用 TryLock 方法是否能成功锁定互斥锁。
- 测试当已有协程锁定互斥锁时,尝试使用 TryLock 方法是否不能成功锁定互斥锁。
- 测试在多个协程并发情况下,使用互斥锁来保护自增操作是否线程安全。
- 测试在未锁定互斥锁的情况下进行解锁操作是否会报错。
- 测试当一协程长时间持有锁时,其他协程尝试获取互斥锁是否会发生饥饿现象。
测试代码
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中提供了互斥锁:sync.Mutex。sync.Mutex提供了以下方法: type Mutex // 加锁。如果已经有goroutine持有了锁,那么就阻塞等待直到持有锁 func (m *Mutex) Lock()// 尝试加锁。如果加锁成功就返回true,否则返回失败 func (m…...
每日三道面试题之 Java并发编程 (一)
1.为什么要使用并发编程 并发编程是一种允许多个操作同时进行的编程技术,这种技术在现代软件开发中非常重要,原因如下: 充分利用多核处理器:现代计算机通常都拥有多核处理器,通过并发编程,可以让每个核心独…...
车身稳定控制系统原理是什么?
车身稳定控制系统(Electronic Stability Control,ESC)是一种先进的车辆动态控制系统,其主要原理是通过传感器监测车辆的各项状态,包括车速、转向角度、侧倾角等,然后通过电子控制单元(ECU&#…...
vue3前端加载动画 lottie-web 的简单使用案例
什么是 Lottie Lottie 是 Airbnb 发布的一款开源动画库,它适用于 Android、iOS、Web 和 Windows 的库。 它提供了一套从设计师使用 AE(Adobe After Effects)到各端开发者实现动画的工具流。 UED 提供动画 json 文件即可, 开发者就…...
基于java+springboot+vue实现的健身房管理系统(文末源码+Lw)23-223
摘 要 传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计算机上安装健身房管理系统软件来发挥其高效地信息处理的作用…...
10款白嫖党必备的ai写作神器,你都知道吗? #媒体#人工智能#其他
从事自媒体运营光靠自己手动操作效率是非常低的,想要提高运营效率就必须要学会合理的使用一些辅助工具。下面小编就跟大家分享一些自媒体常用的辅助工具,觉得有用的朋友可以收藏分享。 1.飞鸟写作 这是一个微信公众号 面向专业写作领域的ai写作工具&am…...
Docker工作流
1.工作流 开发应用编写Dockerfile构建Docker镜像运行Docker容器测试应用发布镜像到Hub迭代更新镜像 2.开发应用 首先你需要创建一个应用,这个应用可以是后端应用或者前端应用,任何语言都可以。 比如:我使用IDEA 创建一个Java后端应用&…...
深入浅出 -- 系统架构之分布式集群的分类
一、单点故障问题 集群,相信诸位对这个概念并不陌生,集群已成为现时代中,保证服务高可用不可或缺的一种手段。 回想起初集中式部署的单体应用,因为只有一个节点,因此当该节点出现任意类型的故障(网络、硬件…...
Docker之镜像与容器的相关操作
目录 一、Docker镜像 搜索镜像 下载镜像 查看宿主机上的镜像 删除镜像 二、Docker容器 创建容器 查看容器 启停容器 删除容器 进入容器 创建/启动/进入容器 退出容器 查看容器内部信息 一、Docker镜像 Docker 运行容器前需要本地存在对应的镜像, 如…...
中科驭数超低时延网络解决方案入选2023年度金融信创优秀解决方案
近日,由中国人民银行领导、中国金融电子化集团有限公司牵头组建的金融信创生态实验室发布「2023年度第三期金融信创优秀解决方案」,中科驭数超低时延网络解决方案从众多方案中脱颖而出,成功入选,代表了该方案的技术创新和金融实践…...
应用方案 | DCDC电源管理芯片MC34063A
MC34063A 为一单片 DC-DC 变换集成电路,内含温度补偿的参考电压源(1.25V)、比较器、能有效限制电流及控制工作周期的振荡器,驱动器及大电流输出开关管等。外配少量元件,就能组成升压、降压及电压反转型 DC-DC 变换器。…...
【个人使用推荐】联机不卡顿 小白一键部署 大厂云服务器选购指南 16G低至26 幻兽帕鲁最大更新来袭
更新日期:4月8日(半年档 价格回调,京东云采购季持续进行) 本文纯原创,侵权必究 《最新对比表》已更新在文章头部—腾讯云文档,文章具有时效性,请以腾讯文档为准! 【腾讯文档实时更…...
57 npm run build 和 npm run serve 的差异
前言 npm run serve 和 npm run build 的差异 这里主要是从 vue-cli 的流程 来看一下 我们经常用到的这两个命令, 他到传递给 webpack 打包的时候, 的一个具体的差异, 大致是配置了那些东西? 经过了那些流程 ? vue-cli 的 vue-plugin 的加载 内置的 plugin 列表如下, 依次…...
原生小程序开发性能优化指南
性能优化指南 1.骨架屏 业务可以在数据加载完成之前用骨架屏幕来占位,提升体验。 2.包大小优化 减小包中静态资源,例如图片文件,可将图片进行压缩降低文件体积。无用文件、函数、样式剔除。除了部分用于容错的图片必须放在代码包…...
「51媒体网」邀请媒体采访报道对企业宣传有何意义?
传媒如春雨,润物细无声的,大家好,我是51媒体网胡老师。 邀请媒体采访报道对企业宣传具有多重意义: 提升品牌知名度和曝光度:媒体是信息传播的重要渠道,通过媒体的报道,企业及其活动、产品能够迅…...
用动态IP采集数据总是掉线是为什么?该怎么解决?
动态IP可以说是做爬虫、采集数据、搜集热门商品信息中必备的代理工具,但在爬虫的使用中,总是会遇到动态IP掉线的情况,从而影响使用效率,本文将探讨动态IP代理掉线的几种常见原因,并提供解决方法,以帮助大家…...
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.概述 程序员通过副业实现个人价值最大化和增加收入的途径多种多样,以下是一些方法: 自由职业: 程序员可以在业余时间提供自由职业服务。包括为客户开发软件、网站或应用程序、…...
【嵌入式开发 Linux 常用命令系列 4.3 -- git add 不 add untracked file】
请阅读【嵌入式开发学习必备专栏 】 文章目录 git add 不add untracked file git add 不add untracked file 如果你想要Git在执行git add .时不添加未跟踪的文件(untracked files),你可以使用以下命令: git add -u这个命令只会加…...
git 常用命令和使用方法
作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生在读,研究方向无线联邦学习 擅长领域:驱动开发,嵌入式软件开发,BSP开发 作者主页:一个平凡而乐于分享的小比特的个人主页…...
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...
【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
均衡后的SNRSINR
本文主要摘自参考文献中的前两篇,相关文献中经常会出现MIMO检测后的SINR不过一直没有找到相关数学推到过程,其中文献[1]中给出了相关原理在此仅做记录。 1. 系统模型 复信道模型 n t n_t nt 根发送天线, n r n_r nr 根接收天线的 MIMO 系…...
Linux离线(zip方式)安装docker
目录 基础信息操作系统信息docker信息 安装实例安装步骤示例 遇到的问题问题1:修改默认工作路径启动失败问题2 找不到对应组 基础信息 操作系统信息 OS版本:CentOS 7 64位 内核版本:3.10.0 相关命令: uname -rcat /etc/os-rele…...
