Go Mutex 错过后悔的重要知识点

news2024/10/5 20:27:24

Go Mutex 的基本用法

Mutex 我们一般只会用到它的两个方法:

  • Lock:获取互斥锁。(只会有一个协程可以获取到锁,通常用在临界区开始的地方。)
  • Unlock: 释放互斥锁。(释放获取到的锁,通常用在临界区结束的地方。)

Mutex 的模型可以用下图表示:

Go Mutex模型.png

说明:

  • 同一时刻只能有一个协程获取到 Mutex 的使用权,其他协程需要排队等待(也就是上图的 G1->G2->Gn)。
  • 拥有锁的协程从临界区退出的时候需要使用 Unlock 来释放锁,这个时候等待队列的下一个协程可以获取到锁(实际实现比这里说的复杂很多,后面会细说),从而进入临界区。
  • 等待的协程会在 Lock 调用处阻塞,Unlock 的时候会使得一个等待的协程解除阻塞的状态,得以继续执行。

这几点也是 Mutex 的基本原理。

Go Mutex 原子操作

Mutex结构体定义:

type Mutex struct {
   state int32 // 状态字段
   sema  uint32 // 信号量
}

其中 state 字段记录了四种不同的信息:

Go Mutex state.png

这四种不同信息在源码中定义了不同的常量:

const (
   mutexLocked      = 1 << iota // 表示有 goroutine 拥有锁
   mutexWoken                   // 唤醒(就是第 2 位)
   mutexStarving                // 饥饿(第 3 位)
   mutexWaiterShift = iota      // 表示第 4 位开始,表示等待者的数量

   starvationThresholdNs = 1e6  // 1ms 进入饥饿模式的等待时间阈值
)

sema 的含义比较简单,就是一个用作不同 goroutine 同步的信号量。

go 的 Mutex 实现中,state 字段是一个 32 位的整数,不同的位记录了四种不同信息,在这种情况下, 只需要通过原子操作就可以保证一次性实现对四种不同状态信息的更改,而不需要更多额外的同步机制。

但是毋庸置疑,这种实现会大大降低代码的可读性,因为通过一个整数来记录不同的信息, 就意味着,需要通过各种位运算来实现对这个整数不同位的修改。

当然,这只是 Mutex 实现中最简单的一种位运算了。下面以 state 记录的四种不同信息为维度来具体讲解一下:

  • mutexLocked:这是 state 的最低位,1 表示锁被占用,0 表示锁没有被占用。

    • new := mutexLocked 新状态为上锁状态
  • mutexWoken: 这是表示是否有协程被唤醒了的状态

    • new = (old - 1<<mutexWaiterShift) | mutexWoken 等待者数量减去 1 的同时,设置唤醒标识
    • new &^= mutexWoken 清除唤醒标识
  • mutexStarving:饥饿模式的标识

    • new |= mutexStarving 设置饥饿标识
  • 等待者数量:state >> mutexWaiterShift 就是等待者的数量,也就是上面提到的 FIFO 队列中 goroutine 的数量

    • new += 1 << mutexWaiterShift 等待者数量加 1
    • delta := int32(mutexLocked - 1<<mutexWaiterShift) 上锁的同时,将等待者数量减 1

在上面做了这一系列的位运算之后,我们会得到一个新的 state 状态,假设名为 new,那么我们就可以通过 CAS 操作来将 Mutexstate 字段更新:

atomic.CompareAndSwapInt32(&m.state, old, new)

通过上面这个原子操作,我们就可以一次性地更新 Mutexstate 字段,也就是一次性更新了四种状态信息。

这种通过一个整数记录不同状态的写法在 sync 包其他的一些地方也有用到,比如 WaitGroup 中的 state 字段。

最后,对于这种操作,我们需要注意的是,因为我们在执行 CAS 前后是没有其他什么锁或者其他的保护机制的, 这也就意味着上面的这个 CAS 操作是有可能会失败的,那如果失败了怎么办呢?

如果失败了,也就意味着肯定有另外一个 goroutine 率先执行了 CAS 操作并且成功了,将 state 修改为了一个新的值。 这个时候,其实我们前面做的一系列位运算得到的结果实际上已经不对了,在这种情况下,我们需要获取最新的 state,然后再次计算得到一个新的 state

所以我们会在源码里面看到 CAS 操作是写在 for 循环里面的。

state的状态及枚举

state状态state状态枚举对应二进制对应状态
mutexUnLockstate=00000未加锁
mutexLockedstate=10001加锁
mutexWokenstate=20010唤醒
mutexStarvingstate=40100饥饿
mutexWaiterShiftstate=30011代表位移

在看下面代码之前,一定要记住这几个状态之间的 与运算 或运算,否则代码里的与运算或运算

state:   |32|31|...|3|2|1|
         __________/ | |
               |      | |
               |      | mutex的占用状态(1被占用,0可用)
               |      |
               |      mutex的当前goroutine是否被唤醒
               |
               当前阻塞在mutex上的goroutine数

互斥锁的作用

互斥锁是保证同步的一种工具,主要体现在以下2个方面:

  1. 避免多个线程在同一时刻操作同一个数据块 (sum)

  2. 可以协调多个线程,以避免它们在同一时刻执行同一个代码块 (sum++)

什么时候用

  1. 需要保护一个数据或数据块时

  2. 需要协调多个协程串行执行同一代码块,避免并发问题时

比如 经常遇到A给B转账100元的例子,这个时候就可以用互斥锁来实现。

注意的坑

1. 不同 goroutine 可以 Unlock 同一个 Mutex,但是 Unlock 一个无锁状态的 Mutex 就会报错。
2. 因为 mutex 没有记录 goroutine_id,所以要避免在不同的协程中分别进行上锁/解锁操作,不然很容易造成死锁。

建议: 先 Lock 再 Unlock、两者成对出现。

3. Mutex 不是可重入锁

Mutex 不会记录持有锁的协程的信息,所以如果连续两次 Lock 操作,就直接死锁了。

如何实现可重入锁?记录上锁的 goroutine 的唯一标识,在重入上锁/解锁的时候只需要增减计数。

type RecursiveMutex struct {
   sync.Mutex
   owner     int64 // 当前持有锁的 goroutine id // 可以换成其他的唯一标识
   recursion 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,调用次数加1
   atomic.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))
   }

   // 调用次数减1
   m.recursion--
   if m.recursion != 0 { // 如果这个 goroutine 还没有完全释放,则直接返回
      return
   }

   // 此 goroutine 最后一次调用,需要释放锁
   atomic.StoreInt64(&m.owner, -1)
   m.Mutex.Unlock()
}
4. 多高的 QPS 才能让 Mutex 产生强烈的锁竞争?

模拟一个 10ms 的接口,接口逻辑中使用全局共享的 Mutex,会发现在较低 QPS 的时候就开始产生激烈的锁竞争(打印锁等待时间和接口时间)。

解决方式:首先要尽量避免使用 Mutex。如果要使用 Mutex,尽量多声明一些 Mutex,采用取模分片的方式去使用其中一个 Mutex 进行资源控制。避免一个 Mutex 对应过多的并发。

简单总结:压测或者流量高的时候发现系统不正常,打开 pprof 发现 goroutine 指标在飙升,并且大量 Goroutine 都阻塞在 Mutex 的 Lock 上,这种现象下基本就可以确定是锁竞争。

5. Mutex 千万不能被复制

因为复制的时候会将原锁的 state 值也进行复制。复制之后,一个新 Mutex 可能莫名处于持有锁、唤醒或者饥饿状态,甚至等阻塞等待数量远远大于0。而原锁 Unlock 的时候,却不会影响复制锁。

关于锁的使用建议:

  • 写业务时不能全局使用同一个 Mutex

  • 千万不要将要加锁和解锁分到两个以上 Goroutine 中进行(容易形成死锁)

  • Mutex 千万不能被复制(包括不能通过函数参数传递),否则会复制传参前锁的状态:已锁定 or 未锁定。很容易产生死锁,关键是编译器还发现不了这个 Deadlock~

  • 尽量避免使用 Mutex,如果非使用不可,尽量多声明一些 Mutex,采用取模分片的方式去使用其中一个 Mutex(分段锁)(尽量减小锁的颗粒度)

参考

  • 标准库文档 —— sync.Mutex

结束语

本篇文章介绍说明了:

sync.Mutex 的基本用法

sync.Mutex 原子操作

sync.Mutex state 的

sync.Mutex 注意的坑

希望本篇文章对你有所帮助,谢谢。

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

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

相关文章

基于STM32麦克风阵列音频信号处理系统设计

v hezkz17进数字音频系统研究开发交流答疑 附录: ADAU1452音频处理系统

飞行动力学 - 第3节-滑翔性能、喷气式飞机最大最小速度 之 基础点摘要

飞行动力学 - 第3节-滑翔性能、喷气式飞机最大最小速度 之 基础点摘要 1. 滑翔机1.1 滑翔动力学方程1.2 滑翔机升阻比1.3 滑翔机性能 2. 喷气式飞机2.1 最小推力2.2 最大、最小速度 3. 参考资料 1. 滑翔机 1.1 滑翔动力学方程 注&#xff1a;这里的航迹角 γ \gamma γ按照右…

基于Python图像的作物种子数统计算法设计与应用(源码+文档+演示视频)

基于图像的作物种子数统计算法设计运用Qt作为开发工具&#xff0c;PyTorch库的使用&#xff0c;算法实现等。具体分为以下三部分&#xff1a;第一部分&#xff0c;采用数码影像技术对黄豆粒进行影像辨识技术。第二部分&#xff0c;前端页面功能包括图像识别技术模块、光源技术模…

音频转文字怎么弄?这几个音频转文字方法可以轻松实现

想象一下&#xff0c;你正在开车或忙着做家务&#xff0c;突然接到一通重要电话&#xff0c;却又没有纸和笔可以记录下来。这是应该怎么办呢&#xff0c;其实我们可以使用录音转文字的软件帮助我们把语音在线转换成文字就能轻松解决这个问题啦&#xff0c;但是又有小伙伴可能会…

考完PMP/NPDP认证,项目经理们最后都找到了什么样的工作?

早上好&#xff0c;我是老原。 有很多粉丝朋友都好奇&#xff0c;考完PMP/NPDP认证&#xff0c;到底能找到一份什么工作&#xff1f; 首先&#xff0c;你要知道一个问题&#xff1a;有工作的人选择考证的几率更大。 有的人是因为职场发展遇到了瓶颈期&#xff0c;想要利用考…

类与对象 (一)

引子 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 C是基于面向对象的&#xff0c;关注的是对象&#xff0c;将一件事情拆分成不同的对象&#xff0c;靠对象之间的交互完成。 什么是对象&#xff1f;…

MYSQL进阶-索引的组合索引

回城传送–》《100天精通MYSQL从入门到就业》 文章目录 一、练习题目二、SQL思路SQL进阶-索引的组合索引初始化数据解法什么是组合索引为什么要使用组合索引如何使用组合索引 答案参考&#xff1a; 一、练习题目 题目链接难度SQL进阶-索引的组合索引★★★☆☆ 二、SQL思路 …

C++初阶之类和对象(中)

类和对象&#xff08;中&#xff09; 1.类的6个默认成员函数2. 构造函数2.1 概念2.2 特性 3.析构函数3.1 概念3.2 特性 4. 拷贝构造函数4.1 概念4.2 特征 5 运算符重载5.1 运算符重载5.2 赋值运算符重载5.3 前置和后置重载 6.日期类的实现7.const成员8.取地址及const取地址操作…

Markdown 进阶语法:Mermaid 绘图 (一) - 流程图 (Flowchart)

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

【Linux】手写一个简易命令行解释器

文章目录 1. 了解命令行解释器1.1 命令行解释器是什么&#xff1f;1.2 我们为什么要尝试手写一个命令行解释器&#xff1f; 2. 命令行解释器的实现2.1 打印提示符2.2 获取用户输入2.3 创建子进程并进行进程程序替换2.4 内建命令 1. 了解命令行解释器 1.1 命令行解释器是什么&a…

TiDB-亿级订单数据亚秒响应查询方案

TiDB-亿级订单数据亚秒响应查询方案 TiDB宣传片 1. 什么是TiDB TiDB 是一个分布式 NewSQL 数据库&#xff0c;它支持水平弹性扩展、ACID 事务、标准 SQL、MySQL 语法和 MySQL 协议&#xff0c;具有数据强一致的高可用特性&#xff0c;是一个不仅适合 OLTP 场景还适合 OLAP 场景…

完全背包问题(二维数组 / 一维数组实现)

完全背包 完全背包的一维和二维dp数组 有 N 件物品和容量为 W 的背包&#xff0c;第 i 件物品的重量是 weight[i]&#xff0c;价值是 value[i] 每件物品都有无限个&#xff0c;即同一物品能够放入背包多次&#xff0c;求背包所能装入物品的最大价值总和 完全背包和 0-1 背包不…

小白到运维工程师自学之路 第四十六集 (mongodb复制集)

一、概述 1、 MongoDB复制集&#xff08;MongoDB Replica Set&#xff09;是MongoDB提供的一种高可用性和数据冗余的解决方案。它由多个MongoDB实例组成&#xff0c;其中一个作为主节点&#xff08;Primary&#xff09;&#xff0c;其他节点则扮演从节点&#xff08;Secondary&…

使用finalshell连接Linux服务器出现的问题

第一次使用finalshell远程连接Linux服务器的过程、遇到的问题及解决方案 首先建立连接 ![在这里插入图片描述](https://img-blog.csdnimg.cn/d8836dcd8a224bf093ebdac031f763d5.png 然后问题来了 出现以下问题&#xff1a; java.net.ConnectException: Connection refused:…

地下污水厂配电能效管理平台设计

安科瑞电气股份有限公司 上海嘉定 201800 摘要&#xff1a;结合某地下污水厂项目&#xff0c;从结构、系统组成、系统功能、控制要求、场景模 式等方面介绍了地下污水厂智能照明控制系统&#xff0c;探索了一套适用于地下污水厂的智能照明控制策略&#xff0c;以确保地下污水…

这里会告诉你音频转换器推荐有什么

音频转换格式技术是一项重要的技术&#xff0c;可以将音频文件从一种格式转换为另一种格式&#xff0c;以适应不同设备、平台或应用程序的需求。今天的文章会向你科普音频转换格式技术和音频转换器推荐有什么。 音频转换格式技术应用于很多场景&#xff1a; 1、音乐制作与后期…

银河麒麟服务器v10 sp1 部署.Net6.0项目后无法访问静态文件

上一篇&#xff1a;银河麒麟服务器v10 sp1 部署.Net6.0 http https_csdn_aspnet的博客-CSDN博客 由于本人项目直接从.NetCore3.1升级到.Net6.0的&#xff0c;请参考文章&#xff1a;NetCore3.1项目升级到Net6.0_vs2022 没有startup_csdn_aspnet的博客-CSDN博客 虽然部署项目后…

mysql根据逗号将一行数据拆分成多行数据,顺便展示其他列

1、原始数据演示 2.处理结果 SQL展示 SELECTa.id,a. NAME,substring_index(substring_index(a.shareholder,,,b.help_topic_id 1),, ,- 1) AS shareholder FROMcompany a JOIN mysql.help_topic b ON b.help_topic_id < (length(a.shareholder) - length(REPLACE (a.share…

uni-app 从零开始第二章:底部 tabBar

pages.json 页面路由 | uni-app官网 一、新建 home页面 找到pages目录&#xff0c;新增一个home的页面&#xff0c;勾选上同时新建文件夹 新建完成后&#xff0c;pages.json 中 会自动添加上刚刚新建的文件信息 二、新增tabBar数据 在 pages.json中新增以下代码 "tabB…

微信保存到本地的视频文件怎么转存到手机笔记?

微信是人们之间交流的重要工具。我们经常会在微信上收到一些珍贵的视频文件&#xff0c;比如亲友们的生活片段、孩子们的成长瞬间等等。但是&#xff0c;随着时间的推移&#xff0c;这些视频文件会越来越多&#xff0c;也会有人担心它们的保存问题。 现在很多人都在使用手机笔…