Golang Mutex 原理详细解析

目录
  • 前言
  • Lock
    • 单协程加锁
    • 加锁被阻塞
  • Unlock
    • 无协程阻塞下的解锁
      • 解锁并唤醒协程
  • 自旋
    • 什么是自旋
    • 自旋条件
    • 自旋的优势
    • 自旋的问题
  • Mutex 的模式
    • Normal 模式
    • Starving 模式
  • Woken 状态

前言

互斥锁是在并发程序中对共享资源进行访问控制的主要手段。对此 Go 语言提供了简单易用的 Mutex。Mutex 和 Goroutine 合作紧密,概念容易混淆,一定注意要区分各自的概念。

Mutex 是一个结构体,对外提供 Lock()Unlock()两个方法,分别用来加锁和解锁。

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
    Lock()
    Unlock()
}

type Mutex struct {
    state int32
    sema  uint32
}

const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota
)
  • Mutex 是一个互斥锁,其零值对应了未上锁的状态,不能被拷贝;
  • state 代表互斥锁的状态,比如是否被锁定;
  • sema 表示信号量,协程阻塞会等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。

注意到 state 是一个 int32 变量,内部实现时把该变量分成四份,用于记录 Mutex 的状态。

  • Locked: 表示该 Mutex 是否已经被锁定,0表示没有锁定,1表示已经被锁定;
  • Woken: 表示是否有协程已经被唤醒,0表示没有协程唤醒,1表示已经有协程唤醒,正在加锁过程中;
  • Starving: 表示该 Mutex 是否处于饥饿状态,0表示没有饥饿,1表示饥饿状态,说明有协程阻塞了超过1ms;

上面三个表示了 Mutex 的三个状态:锁定 - 唤醒 - 饥饿。

Waiter 信息虽然也存在 state 中,其实并不代表状态。它表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。

协程之间的抢锁,实际上争抢给Locked赋值的权利,能给 Locked 置为1,就说明抢锁成功。抢不到就阻塞等待 sema 信号量,一旦持有锁的协程解锁,那么等待的协程会依次被唤醒。

Woken 和 Starving 主要用于控制协程间的抢锁过程。

Lock

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

若当前锁已经被使用,请求 Lock() 的 goroutine 会阻塞,直到锁可用为止。

单协程加锁

若只有一个协程加锁,无其他协程干扰,在加锁过程中会判断 Locked 标志位是否为 0,若当前为 0 则置为 1,代表加锁成功。这里本质是一个 CAS 操作,依赖了 atomic.CompareAndSwapInt32

加锁被阻塞

假设协程B在尝试加锁前,已经有一个协程A获取到了锁,此时的状态为:

此时协程B尝试加锁,被阻塞,Mutex 的状态为:

Waiter 计数器增加了1,协程B将会持续阻塞,直到 Locked 值变成0 后才会被唤醒。

Unlock

func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// Fast path: drop lock bit.
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// Outlined slow path to allow inlining the fast path.
		// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
		m.unlockSlow(new)
	}
}

如果 Mutex 没有被加锁,就直接 Unlock ,会抛出一个 runtime error。

从源码注释来看,一个 Mutex 并不会与某个特定的 goroutine 绑定,理论上讲用一个 goroutine 加锁,另一个 goroutine 解锁也是允许的,不过为了代码可维护性,一般还是建议不要这么搞。

A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it.

无协程阻塞下的解锁

假定在解锁时,没有其他协程阻塞等待加锁,那么只需要将 Locked 置为 0 即可,不需要释放信号量。

解锁并唤醒协程

假定解锁时有1个或多个协程阻塞,解锁过程分为两个步骤:

  • Locked位置0;
  • 看到 Waiter > 0,释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程把 Locked 置为1,获取到锁。

自旋

加锁时,如果当前 Locked 位为1,则说明当前该锁由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续探测 Locked 位是否变为0,这个过程就是「自旋」。

自旋的时间很短,如果在自旋过程中发现锁已经被释放,那么协程可以立即获取锁。此时即便有协程被唤醒,也无法获取锁,只能再次阻塞。

自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免一部分协程的切换。

什么是自旋

自旋对应于 CPU 的 PAUSE 指令,CPU 对该指令什么都不做,相当于空转。对程序而言相当于sleep了很小一段时间,大概 30个时钟周期。连续两次探测Locked 位的间隔就是在执行这些 PAUSE 指令,它不同于sleep,不需要将协程转为睡眠态。

自旋条件

加锁时 Golang 的 runtime 会自动判断是否可以自旋,无限制的自旋将给 CPU 带来巨大压力,自旋必须满足以下所有条件:

  • 自旋次数要足够少,通常为 4,即自旋最多 4 次;
  • CPU 核数要大于 1,否则自旋没有意义,因为此时不可能有其他协程释放锁;
  • 协程调度机制中的 P 的数量要大于 1,比如使用 GOMAXPROCS() 将处理器设置为 1 就不能启用自旋;
  • 协程调度机制中的可运行队列必须为空,否则会延迟协程调度。

可见自旋的条件是很苛刻的,简单说就是不忙的时候才会启用自旋。

自旋的优势

自旋的优势是更充分地利用 CPU,尽量避免协程切换。因为当前申请加锁的协程拥有 CPU,如果经过短时间的自旋可以获得锁,则当前写成可以继续运行,不必进入阻塞状态。

自旋的问题

如果在自旋过程中获得锁,那么之前被阻塞的协程就无法获得。如果加锁的协程特别多,每次都通过自旋获取锁,则之前被阻塞的协程将很难获取锁,从而进入【饥饿状态】。

为此,Golang 1.8 版本后为Mutex增加了Starving模式,在这个状态下不会自旋,一旦有协程释放锁。那么一定会唤醒一个协程并成功加锁。

Mutex 的模式

每个 Mutex 都有两种模式:NormalStarving

Normal 模式

默认情况下的模式就是 Normal。 在该模式下,协程如果加锁不成功,不会立即转入阻塞排队(先进先出),而是判断是否满足自旋条件,如果满足则会启动自旋过程,尝试抢锁。

Starving 模式

自旋过程中能抢到锁,一定意味着同一时刻有协程释放了锁。我们知道释放锁时,如果发现有阻塞等待的协程,那么还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到 CPU 后开始运行,此时发现锁已经被抢占了,自己只好再次阻塞,不过阻塞前会判断,自上次阻塞到本次阻塞经过了多长时间,如果超过 1ms,则会将 Mutex 标记为 Starving模式,然后阻塞。

Starving模式下,不会启动自旋过程,一旦有协程释放了锁,一定会唤醒协程,被唤醒的协程将成功获取锁,同时会把等待计数减 1。

Woken 状态

Woken 状态用于加锁和解锁过程中的通信。比如,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把 Woken 标记为 1,用于通知解锁协程不必释放信号量,类似知会一下对方,不用释放了,我马上就拿到锁了。

到此这篇关于Golang Mutex 原理详细解析的文章就介绍到这了,更多相关Golang Mutex 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang 基于 flag 库实现一个简单命令行工具

    目录 前言 flag 库 FlagSet 需求拆解 实现 weather flag 天气数据打印 获取源数据 数据转换 运行效果 小结 前言 Golang 标准库中的 flag 库提供了解析命令行选项的能力,我们可以基于此来开发命令行工具. 假设我们想做一个命令行工具,我们通过参数提供[城市],它自动能够返回当前这个[城市]的天气状况.这样一个简单的需求,今天我们就来试一下,看怎样实现. flag 库 Package flag implements command-line flag parsi

  • Golang 手写一个简单的并发任务 manager

    目录 前言 errgroup 需求拆解 实战代码 Job JobManager 错误处理 及时退出 完整代码 小结 前言 今天也是偏实战的内容,作为一个并发复习课,很简单,我们来看看怎样实现一个并发任务 manager. 在微服务的场景下,我们有很多任务的执行是没有明确的先后顺序的,比如一个接口同时要做到任务 A 和 任务 B,两个任务分别拿到一些数据,最后组装裁剪后通过接口下发. 此时,A 和 B 两个任务没有依赖关系,如果我们串行来执行,会拖慢整个任务的执行节奏,用并发的方式来优化是一个方向

  • 一文搞懂Golang中iota的用法和原理

    目录 前言 iota的使用 iota在const关键字出现时将被重置为0 按行计数 所有注释行和空行全部忽略 跳值占位 多个iota 一行多个iota 首行插队 中间插队 没有表达式的常量定义复用上一行的表达式 实现原理 iota定义 const 前言 我们知道iota是go语言的常量计数器,只能在常量的const表达式中使用,在const关键字出现的时将被重置为0,const中每新增一行常量声明iota值自增1(iota可以理解为const语句块中的行索引),使用iota可以简化常量的定义,但

  • Golang errgroup 设计及实现原理解析

    目录 开篇 errgroup 源码拆解 Group WithContext Wait Go SetLimit TryGo 使用方法 结束语 开篇 继上次学习了信号量 semaphore 扩展库的设计思路和实现之后,今天我们继续来看 golang.org/x/sync 包下的另一个经常被 Golang 开发者使用的大杀器:errgroup. 业务研发中我们经常会遇到需要调用多个下游的场景,比如加载一个商品的详情页,你可能需要访问商品服务,库存服务,券服务,用户服务等,才能从各个数据源获取到所需要的

  • golang生成JSON以及解析JSON

    目录 一.JSON解析到结构体 1.JSON名称/值对 2.JSON值 3.JSON数据 4.JSON对象 5.JSON数组 6.JSON布尔值 7.JSON null 二.JSON转map 三.生成JSON 一.JSON解析到结构体 在介绍这部分之前先简要介绍一下Json语法 JSON 语法是 JavaScript 语法的子集.JSON 语法是 JavaScript 对象表示法语法的子集. 数据在名称/值对中 数据由逗号分隔 大括号保存对象 中括号保存数组 1.JSON名称/值对 "name&

  • golang值接收者和指针接收者的区别介绍

    目录 方法 接口实现 两者分别在何时使用 方法 方法能给用户自定义的类型添加新的行为.它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法.接收者可以是值接收者,也可以是指针接收者.在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法:指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法. package main import "fmt" type Person struct { age int } func (p Pers

  • Golang Mutex 原理详细解析

    目录 前言 Lock 单协程加锁 加锁被阻塞 Unlock 无协程阻塞下的解锁 解锁并唤醒协程 自旋 什么是自旋 自旋条件 自旋的优势 自旋的问题 Mutex 的模式 Normal 模式 Starving 模式 Woken 状态 前言 互斥锁是在并发程序中对共享资源进行访问控制的主要手段.对此 Go 语言提供了简单易用的 Mutex.Mutex 和 Goroutine 合作紧密,概念容易混淆,一定注意要区分各自的概念. Mutex 是一个结构体,对外提供 Lock()和Unlock()两个方法,

  • golang切片原理详细解析

    目录 切片的解析 切片的初始化 字面量初始化 make初始化 切片的截取 切片的复制 切片的扩容 总结 切片的解析 当我们的代码敲下[]时,便会被go编译器解析为抽象语法树上的切片节点, 被初始化为切片表达式SliceType: // go/src/cmd/compile/internal/syntax/parser.go // TypeSpec = identifier [ TypeParams ] [ "=" ] Type . func (p *parser) typeDecl(g

  • vue.js diff算法原理详细解析

    目录 diff算法的概念 虚拟Dom h函数 diff对比规则 patch patchVnode updateChildren 总结 diff算法的概念 diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom.顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom. 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟

  • Tomcat中的catalina.bat原理详细解析

    前言 本文主要给大家详细解析了关于Tomcat中catalina.bat原理的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. tomcat 的真正启动是在 catalina.bat 设置并启动的.startup.bat 只是找到catalina.bat 然后执行catalina.bat 来启动tomat的.下面我们来分析下catalina.bat 验证CATALINA_HOME 环境变量 验证CATALINA_HOME 设置是否正确,如果不正确,重新设置CATALIN

  • Tomcat中的startup.bat原理详细解析

    前言 在刚开始接触计算机,一开始就是win2000,所以对批处理脚本命令都不会.平时启TOMCAT都是鼠标双击startup.bat了,很少看过里面写的是什么,也借学习TOMCAT的机会学习一下批处理的常用命令,不求都记住,但求以后再见到批处理命令能看的懂,说的出是干什么的.本文主要给大家介绍了关于Tomcat中startup.bat原理的相关内容,下面话不多说了,来一起看看详细的介绍吧. startup.bat 解析 验证CATALINA_HOME 环境变量是否设置,如果没有设置则通过CATA

  • Android 操作系统获取Root权限 原理详细解析

    android root权限破解分析 许多机友新购来的Android机器没有破解过Root权限,无法使用一些需要高权限的软件,以及进行一些高权限的操作,其实破解手机Root权限是比较简单及安全的,破解Root权限的原理就是在手机的/system/bin/或/system/xbin/目录下放置一个可执行文件"su",这是一个二进制文件,相当于电脑上的exe文件,仅仅在系统中置入这个"su"文件是不会给手机的软件或硬件造成任何故障. 下面的代码是android系统原版的

  • C++多态的实现及原理详细解析

    1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数.2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的.3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性.4. 多态用虚函数来实现,结合动态绑定.5. 纯虚函数是虚函数再加上= 0.6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()=0:即抽象类!必须在子类实现这个函数!

  • Golang底层原理解析String使用实例

    目录 引言 String底层 stringStruct结构 引言 本人因为种种原因(说来听听),放弃大学学的java,走上了golang这条路,本着干一行爱一行的情怀,做开发嘛,不能只会使用这门语言,所以打算开一个底层原理系列,深挖一下,狠狠的掌握一下这门语言 废话不多说,上货 String底层 既然研究底层,那就得全方面覆盖,必须先搞一下基础的东西,那必须直接基本数据类型走起啊, 字符串String的底层我看就很基础 string大家应该都不陌生,go中的string是所有8位字节字符串的集合

  • Docker网络原理及自定义网络详细解析

    Docker在宿主机上虚拟了一个网桥,当创建并启动容器的时候,每一个容器默认都会被分配一个跟网桥网段一致的ip,网桥作为容器的网关,网桥与每一个容器联通,容器间通过网桥可以通信.由于网桥是虚拟出来的,外网无法进行寻址,也就是默认外网无法访问容器,需要在创建启动容器时把宿主机的端口与容器端口进行映射,通过宿主机IP端口访问容器.这是Docker默认的网络,它有一个弊端是只能通过IP让容器互相访问,如果想使用容器名称或容器ID互相访问需要在创建启动容器时候用link的方式修改hosts文件实现.一般

  • MySQL索引机制的详细解析及原理

    目录 一.索引的类型与常见的操作 二.常见的索引详解与创建 三.索引的原理 1.通过实验介绍B+tree 2.延伸 四.聚簇索引和非聚簇索引 1.使用聚簇索引的优势 2.什么情况下无法使用索引 总结 一.索引的类型与常见的操作 前缀索引 MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度.但是前缀索引也有它的坏处:MySQL 不能在 ORDER BY 或 GROUP BY 中使用前缀索引,也不能把它们用作覆盖索引(Covering Index). 复合索引 集一个索引包含多个列(最左前

随机推荐