Go 互斥锁 Mutex 源码分析(二)

news/2024/9/13 11:44:53/文章来源:https://www.cnblogs.com/xingzheanan/p/18377669

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

在 Go 互斥锁 Mutex 源码分析(一) 一文中分析了互斥锁的结构和基本的抢占互斥锁的场景。在学习锁的过程中,看的不少文章是基于锁的状态解释的,个人经验来看,从锁的状态出发容易陷入细节,了解锁的状态转换过一段时间就忘,难以做到真正的理解。想来是用静态的方法分析动态的问题导致的。在实践中发现结合场景分析互斥锁对笔者来说更加清晰,因此有了 Go 互斥锁 Mutex 源码分析(一),本文接着结合不同场景分析互斥锁。

1. 不同场景下的锁状态

1.1 唤醒 goroutine

给出示意图:

image

G1 通过 Fast path 拿到锁,G2 在自旋之后,锁还是已锁状态。这是和 Go 互斥锁 Mutex 源码分析(一) 中的场景不一样的地方。接着自旋之后看,这种场景下会发生什么:

func (m *Mutex) lockSlow() {...for {if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {...}// step2: 当前锁未释放,old = 1new := old// step2: 如果当前锁是饥饿的,跳过期望状态 new 的更新// -      这里锁不是饥饿锁,new = old = 1if old&mutexStarving == 0 {new |= mutexLocked}// step2: 当前锁未释放,更新 new// -      更新 new 的等待 goroutine 位,表示有一个 goroutine 等待// -      更新 new 为 1001,new = 9 if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// step2: 当前 goroutine 不是饥饿状态,跳过 new 更新if starving && old&mutexLocked != 0 {new |= mutexStarving}// step2: 当前 goroutine 不是唤醒状态,跳过 new 更新if awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}// step3: 原子 CAS 更新锁的状态// -      这里更新锁 m.state = 1 为 m.state = new = 9// -      表示当前有一个 goroutine 在等待锁if atomic.CompareAndSwapInt32(&m.state, old, new) {...// waitStartTime = 0, queueLifo = falsequeueLifo := waitStartTime != 0if waitStartTime == 0 {// 更新 waitStartTimewaitStartTime = runtime_nanotime()}// step4: 调用 runtime_SemacquireMutex 阻塞 goroutineruntime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs...}}
}

Mutex.lockSlow 中更新了锁状态,接着进入 runtime_SemacquireMutexruntime_SemacquireMutex 是个非常重要的函数,我们有必要介绍它。

runtime_SemacquireMutex 接收三个参数。其中,重点是信号量 &m.semaqueueLifo。如果 queueLifo = false,当前 goroutine 将被添加到等待锁队列的队尾,阻塞等待唤醒。

G2 执行到 runtime_SemacquireMutex 时将进入阻塞等待唤醒状态,那么怎么唤醒 G2 呢? 我们需要看解锁过程。

1.1.1 sync.Mutex.Unlock

在 G2 阻塞等待唤醒时,G1 开始释放锁。进入 sync.Mutex.Unlock

func (m *Mutex) Unlock() {...// 将 m.state 的锁标志位置为 0,表示锁已释放new := atomic.AddInt32(&m.state, -mutexLocked)// 检查 new 是否为 0,如果为 0 则表示当前无 goroutine 等待,直接退出// 这里 new = 9,G2 在等待唤醒if new != 0 {m.unlockSlow(new)}
}

进入 Mutex.unlockSlow

func (m *Mutex) unlockSlow(new int32) {// 检查锁是否已释放,释放一个已经释放的锁将报错if (new+mutexLocked)&mutexLocked == 0 {fatal("sync: unlock of unlocked mutex")}// 检查锁是普通锁还是饥饿锁if new&mutexStarving == 0 {// 这里 new = 8 是普通锁,进入处理普通锁逻辑old := newfor {// 如果没有 goroutine 等待,则返回if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// old 的唤醒位置 1,并且将等待的 goroutine 减 1,表示将唤醒一个等待中的 goroutine// 这里 new = 2new = (old - 1<<mutexWaiterShift) | mutexWoken// m.state = 8, old = 8, new = 2// CAS 更新 m.state = new = 2if atomic.CompareAndSwapInt32(&m.state, old, new) {// 进入 runtime_Semrelease 唤醒 goroutineruntime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// 处理饥饿锁逻辑,暂略runtime_Semrelease(&m.sema, true, 1)}
}

sync.Mutex.Unlock 中的 runtime_Semrelease 唤醒队列中等待的 goroutine。其中,主要接收信号量 &m.semahandoff 两个参数。这里 handoff = false,将增加信号量,唤醒队列中等待的 goroutine G2。

1.1.2 唤醒 G2

唤醒之后,G2 继续执行后续代码:

func (m *Mutex) lockSlow() {...for {...if atomic.CompareAndSwapInt32(&m.state, old, new) {...runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 检查唤醒的 goroutine 是否是饥饿模式// 如果是饥饿模式,或等待锁时间超过 1ms 则将 goroutine 置为饥饿模式// 注意这是 goroutine 是饥饿的,不是锁是饥饿锁starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs// m.state 在 G1 unlock 时被更新为 2old = m.state// 锁不是饥饿锁,跳过if old&mutexStarving != 0 {...}awoke = trueiter = 0}}
}

唤醒后的 G2 将 old 更新为 2。信号量增加,释放锁,只会唤醒一个 goroutine,被唤醒的 goroutine,这里是 G2,将继续循环:

func (m *Mutex) lockSlow() {...for {// old = 2,不会进入自旋if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {...}// 更新 new:new 是期望 goroutine 更新的状态// 这里 new = old = 2new := old// old = 2,不是饥饿锁// 更新 new 为 011,3if old&mutexStarving == 0 {new |= mutexLocked}// old = 2,表示锁已释放,不会将 goroutine 加入等待位if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 不饥饿,跳过if starving && old&mutexLocked != 0 {new |= mutexStarving}// awoke = trueif awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}// 重置唤醒位,将 new 更新为 001,1new &^= mutexWoken}// m.state = 2, old = 2, new =1// CAS 更新 m.state= new = 1,表示当前 goroutine 已加锁if atomic.CompareAndSwapInt32(&m.state, old, new) {// 当前 goroutine 已加锁跳出循环if old&(mutexLocked|mutexStarving) == 0 {break // locked the mutex with CAS}...}}
}

在循环一轮后,G2 将拿到锁,接着执行临界区代码,最后在释放锁。

这里的场景是唤醒之后,goroutine 不饥饿。那么饥饿锁又是如何触发的呢?我们继续看饥饿锁的场景。

1.2 饥饿锁

饥饿锁场景下的示意图如下:

image

当 G1 释放锁时,G3 正在自旋等待锁释放。当 G1 释放锁时,被唤醒的 G2 和自旋的 G3 竞争大概率会拿不到锁。Go 在 1.9 中引入互斥锁的 饥饿模式 来确保互斥锁的公平性。

对于互斥锁循环中的大部分流程,我们在前两个场景下也过了一遍,这里有重点的摘写,以防赘述。

首先,还是看 G2,当 G1 释放锁时,G2 被唤醒,执行后续代码。如下:

func (m *Mutex) lockSlow() {...for {...if atomic.CompareAndSwapInt32(&m.state, old, new) {...runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 唤醒 G2,G2 等待锁时间超过 1ms// starving = truestarving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs// 锁被 G3 抢占,m.state = 0011old = m.state// 这时候 old 还不是饥饿锁,跳过if old&mutexStarving != 0 {...}awoke = trueiter = 0}}
}

唤醒 G2 之后,G2 等待锁时间超过 1ms 进入饥饿模式。接着进入下一轮循环:

func (m *Mutex) lockSlow() {...for {// old 是唤醒锁,不会进入自旋if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {...}// 锁的期望状态,new = old = 0011new := old// 锁不是饥饿锁,更新 new 的锁标志位为已锁// new = 0011if old&mutexStarving == 0 {new |= mutexLocked}// 锁如果是饥饿或者已锁状态更新 goroutine 等待位// new = 1011if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// goroutine 饥饿,且锁已锁// 更新 new 为饥饿状态,new = 1111if starving && old&mutexLocked != 0 {new |= mutexStarving}// 这里 G2 是唤醒的,重置唤醒位// new = 1101if awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}// CAS 更新 m.state = new = 1101if atomic.CompareAndSwapInt32(&m.state, old, new) {...// G2 入队列过,这里 queueLifo = truequeueLifo := waitStartTime != 0// 将 G2 重新加入队列,并加入到队首,阻塞等待runtime_SemacquireMutex(&m.sema, queueLifo, 1)...}}
}

G2 进入饥饿模式,将互斥锁置为饥饿模式,当前互斥锁状态为 m.state = 1101。G2 作为队列中的队头,阻塞等待锁释放。

类似的,我们看 G3 释放锁的过程。

1.2.1 释放饥饿锁

G3 开始释放锁:

func (m *Mutex) Unlock() {...// new = 1100new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {// 进入 Mutex.unlockSlowm.unlockSlow(new)}
}func (m *Mutex) unlockSlow(new int32) {...// new = 1100,是饥饿锁if new&mutexStarving == 0 {...} else {// 进入处理饥饿锁逻辑// handoff = true,直接将队头阻塞的 goroutine 唤醒runtime_Semrelease(&m.sema, true, 1)}
}

1.2.2 饥饿锁唤醒

在一次的在队头中阻塞的 G2 被唤醒,接着执行唤醒后的代码:

func (m *Mutex) lockSlow() {...for {...if atomic.CompareAndSwapInt32(&m.state, old, new) {...runtime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// old = 1100,是饥饿锁if old&mutexStarving != 0 {...// delta = -(1001)delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {...// delta = -(1101)delta -= mutexStarving}//更新互斥锁状态 m.state = 0001,退出循环atomic.AddInt32(&m.state, delta)break}}}
}

唤醒之后的 G2 直接获得锁,将互斥锁状态置为已锁,直到释放。

2. 锁状态流程

前面我们根据几个场景给出了互斥锁的状态转换过程,这里直接给出互斥锁的流程图如下:

image

3. 总结

本文是 Go 互斥锁 Mutex 源码分析的第二篇,进一步通过两个场景分析互斥锁的状态转换。互斥锁的状态转换如果陷入状态更新,很容易头晕,这里通过不同场景,逐步分析,整个状态,接着给出状态转换流程图,力图做到源码层面了解锁的状态转换。


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

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

相关文章

REST framework:分页

REST framework提供了分页的支持 一、全局配置(不建议使用) 在配置文件中设置全局的分页方式:REST_FRAMEWORK = {DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination,PAGE_SIZE: 10 # 每页数据量 }二、局部配置 在不同的视图中可以通过pagination_c…

052、Vue3+TypeScript基础,页面通讯之一个组件中多个v-model数据绑定

01、main.js代码如下:// 引入createApp用于创建Vue实例 import {createApp} from vue // 引入App.vue根组件 import App from ./App.vue// 引入emitter用于全局事件总线 // import emitter from @/utils/emitterconst app = createApp(App);// App.vue的根元素id为app app.mou…

React 和 Vite 环境下 TailwindCSS 的配置指南

1. 安装tailwindcss npm install -D tailwindcss postcss autoprefixer2. 生成tailwindcss 配置文件 npx tailwind init -p3. tailwind.config.js 配置 /** @type {import(tailwindcss).Config} */ export default {content: ["./index.html","./src/**/*.{js,t…

051、Vue3+TypeScript基础,页面通讯之v-model在组件中手写实现

01、main.js代码如下:// 引入createApp用于创建Vue实例 import {createApp} from vue // 引入App.vue根组件 import App from ./App.vue// 引入emitter用于全局事件总线 // import emitter from @/utils/emitterconst app = createApp(App);// App.vue的根元素id为app app.mou…

docker 修改容器内容后更新镜像的流程

在 Docker 中,如果你修改了一个容器的内容并希望将这些更改保存为一个新的镜像,可以按照以下步骤进行: docker version: 26.1 1. 确保容器运行 首先,确保你正在修改的容器是运行中的。如果容器已经停止,你需要启动它: docker start <container_id> 2. 进入容器并进…

REST framework:排序过滤器的使用

对于列表数据,REST framework提供了OrderingFilter过滤器来帮助我们快速指明数据按照指定字段进行排序 1、在setting中的REST_FRAMEWORK添加配置DEFAULT_FILTER_BACKENDS: (# 这个是指定使用django_filters中的过滤器来进行过滤django_filters.rest_framework.DjangoFilterBac…

AP5160 电压2.5-100V 电流12A PWM 调光 大功率LED驱动 手电筒与摩托车照明方案

产品描述 AP5160 是一款效率高,稳定可靠的 LED 灯恒流驱动控制芯片,内置高精度比较器,固定 关断时间控制电路,恒流驱动电路等,特别适合大功率 LED 恒流驱动。 AP5160采用SOT23-6封装,通过调节外置电流检测的电阻值来设置流过LED 灯的电流,从而设置LED灯的亮度,外驱 MOS…

解决方案 | VS2022 社区版 获取工具和功能找不到visual stdio安装程序的终极解决办法

首先这是一种解决方法: https://blog.csdn.net/Wysnbb/article/details/124588395其次,如果上面方法解决不了,那么可以重新下载vs 社区版。(不要误会,并不是下载10G+的东西) https://visualstudio.microsoft.com/zh-hans/vs/community/下载得到:安装VisualStudioSetup.e…

折腾 Quickwit,Rust 编写的分布式搜索引擎(专为从对象存储中实现亚秒级搜索而设计)

什么是 Quickwit? Quickwit 是首个能在云端存储上直接执行复杂的搜索与分析查询的引擎,并且具有亚秒级延迟。它借助 Rust 语言和分离计算与存储的架构设计,旨在实现资源高效利用、易于操作以及能够扩展到 PB 级数据量。 Quickwit 非常适合日志管理、分布式追踪以及通常为不可…

代码随想录day39 || 198 打家劫舍,213 打家劫舍||,337 打家劫舍|||

198 打家劫舍 func rob(nums []int) int {// 思路,动态规划// dp[i] 代表前下标为i能装的最大盗窃物品价值// 递推 dp[i] = max(dp[i-1], dp[i-2]+v(i)) // dp[i-1] 代表不偷物品i, dp[i-2]+v(i) 代表偷物品i,那么就不能偷i-1,因为题目要求不能相邻,所以考虑前i-2// dp[0]…

CF 做题笔记

在这个随笔中,会有笔者的一些 CodeForces 做题笔记,包括但不限于思想、解题技巧、代码实现等。CF1926G Vlad and Trouble at MIT \(\texttt{*1900}\)。TAG: \(\texttt{树形 dp}\) \(dp_{i,S,P}\) 为 \(i\) 的子树内是否存在 S 和 P 的状态。 转移方程为:当 \(s_i\) 为 C 时 …