当前位置:网站首页>golang中的Mutex原理解析
golang中的Mutex原理解析
2022-07-06 17:22:00 【raoxiaoya】
Mutex结构
type Mutex struct {
state int32
sema uint32
}
state表示互斥锁的状态。
sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。
- Locked::表示该Mutex是否已被锁定,0-没有锁定,1-已被锁定。
- Woken::表示是否有协程已被唤醒,0-没有协程唤醒,1-已有协程唤醒,正在加锁过程中。
- Starving:表示该Mutex是否处于饥饿状态, 0-没有饥饿,1-饥饿状态,说明有协程阻塞了超过1ms。
- Waiter::表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待Mutex.sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。
Woken 和 Starving 主要用于控制协程间的抢锁过程,后面再进行了解。
在阅读源码的时候,你会发现各种位运算逻辑运算实在不太好阅读,为什么不使用四个独立的字段来表示呢,实际上这样是为了原子操作,试想一下,如何能保证原子性的同时更新这四个字段呢?所以,把他们合成到一个字段并配合atomic就能解决这个问题。
Mutex的方法
Mutext对外提供两个方法:
Lock() 加锁方法
Unlock() 解锁方法
下面我们分析一下加锁和解锁的过程,加锁分成功和失败两种情况,成功的话直接获取锁,失败后当前协程被阻塞,同样,解锁时根据是否有阻塞协程也有两种处理。
简单加锁
假定当前只有一个协程在加锁,没有其他协程干扰,那么过程如下图所示:
加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功。从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化。
加锁被阻塞
假定加锁时,锁已被其他协程占用了,此时加锁过程如下图所示:
从上图可看到,当协程B对一个已被占用的锁再次加锁时,Waiter计数器增加了1,此时协程B将被阻塞,直到Locked值变为0后才会被唤醒。
简单解锁
假定解锁时,没有其他协程阻塞,此时解锁过程如下图所示:
由于没有其他协程阻塞等待加锁,所以此时解锁时只需要把Locked位置为0即可,不需要释放信号量。
解锁并唤醒协程
假定解锁时,有1个或多个协程阻塞,此时解锁过程如下图所示:
协程A解锁过程分为两个步骤,一是把Locked位置0,二是查看到Waiter>0,所以释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程B把Locked位置1,于是协程B获得锁。
自旋过程
加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程spin。
自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
自旋操作,会调用procyield函数,该函数也是汇编语言实现。函数内部循环调用PAUSE指令。PAUSE指令什么都不做,但是会消耗CPU时间,因此不会让出CPU。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的上下文切换。
自旋条件
加锁时程序会自动判断是否可以自旋,无限制的自旋将会给CPU带来巨大压力,所以判断是否可以自旋就很重要了。
自旋必须满足以下所有条件:
- 自旋次数要足够小,通常为4,即自旋最多4次。
- CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁。
- 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋。
- 协程调度机制中的可运行队列必须为空,否则会延迟协程调度。
- 可见,自旋的条件是很苛刻的,总而言之就是不忙的时候才会启用自旋。
自旋的优势
自旋的优势是更充分的利用CPU,尽量避免协程切换。因为当前申请加锁的协程拥有CPU,如果经过短时间的自旋可以获得锁,当前协程可以继续运行,不必进入阻塞状态。
自旋的问题
如果自旋过程中获得锁,那么之前被阻塞的协程将无法获得锁,如果加锁的协程特别多,每次都通过自旋获得锁,那么之前被阻塞的进程将很难获得锁,从而进入饥饿状态。
为了避免协程长时间无法获取锁,自1.8版本以来增加了一个状态,即Mutex的Starving状态。这个状态下不会自旋,一旦有协程释放锁,那么一定会唤醒一个协程并成功加锁。
Mutex模式
前面分析加锁和解锁过程中只关注了Waiter和Locked位的变化,现在我们看一下Starving位的作用。
每个Mutex都有两个模式,称为Normal和Starving。下面分别说明这两个模式。
normal模式
默认情况下,Mutex的模式为normal。
该模式下,协程如果加锁不成功不会立即转入阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋过程,尝试抢锁。
starvation模式
自旋过程中能抢到锁,一定意味着同一时刻有协程释放了锁,我们知道释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为"饥饿"模式,然后再阻塞。
处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。
Woken状态
Woken状态用于加锁和解锁过程的通信,举个例子,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把Woken标记为1,用于通知解锁协程不必释放信号量了,好比在说:你只管解锁好了,不必释放信号量,我马上就拿到锁了。
为什么重复解锁要panic
可能你会想,为什么Go不能实现得更健壮些,多次执行Unlock()也不要panic?
仔细想想Unlock的逻辑就可以理解,这实际上很难做到。Unlock过程分为将Locked置为0,然后判断Waiter值,如果值>0,则释放信号量。
如果多次Unlock(),那么可能每次都释放一个信号量,这样会唤醒多个协程,多个协程唤醒后会继续在Lock()的逻辑里抢锁,势必会增加Lock()实现的复杂度,也会引起不必要的协程切换。
总结一下
源代码看起来挺不好阅读的,位运算逻辑运算,主要是为了实现atomic操作,Mutex最核心的逻辑其实就是信号量的PV操作,而state 的四个状态就是为了优化用的。
在m.lockSlow()
和 m.unlockSlow()
之前都有一个状态修改的操作,这个是为了防止重复释放信号量的,因为释放信号量并不会被阻塞,而重复释放信号量会破坏mutex的逻辑。可以参考 https://www.cnblogs.com/niniwzw/p/3153955.html
源代码中,借助了原子操作atomic来实现Mutex,那是因为atomic是由硬件提供的支持(CPU指令),粒度更小,性能更高,而信号量是由操作系统提供的。
关于信号量
我们知道信号量,是操作系统提供的,用来实现互斥锁和线程同步的,粒度为线程级别的,而golang的Mutex是协程级别的,显然无法直接使用操作系统信号量的,因此这里面还需要配合golang的协程调度模型GMP来进一步理解。
边栏推荐
- [C language] dynamic address book
- 第七篇,STM32串口通信编程
- Do you understand this patch of the interface control devaxpress WinForms skin editor?
- 再聊聊我常用的15个数据源网站
- 重上吹麻滩——段芝堂创始人翟立冬游记
- 【批處理DOS-CMD命令-匯總和小結】-字符串搜索、查找、篩選命令(find、findstr),Find和findstr的區別和辨析
- Explain in detail the matrix normalization function normalize() of OpenCV [norm or value range of the scoped matrix (normalization)], and attach norm_ Example code in the case of minmax
- 一行代码实现地址信息解析
- 详解OpenCV的矩阵规范化函数normalize()【范围化矩阵的范数或值范围(归一化处理)】,并附NORM_MINMAX情况下的示例代码
- C Primer Plus Chapter 14 (structure and other data forms)
猜你喜欢
【JVM调优实战100例】04——方法区调优实战(上)
[batch dos-cmd command - summary and summary] - string search, search, and filter commands (find, findstr), and the difference and discrimination between find and findstr
[software reverse automation] complete collection of reverse tools
BFS realizes breadth first traversal of adjacency matrix (with examples)
Data sharing of the 835 postgraduate entrance examination of software engineering in Hainan University in 23
Maidong Internet won the bid of Beijing life insurance to boost customers' brand value
建立自己的网站(17)
Batch obtain the latitude coordinates of all administrative regions in China (to the county level)
做微服务研发工程师的一年来的总结
View remote test data and records anytime, anywhere -- ipehub2 and ipemotion app
随机推荐
【批处理DOS-CMD命令-汇总和小结】-跳转、循环、条件命令(goto、errorlevel、if、for[读取、切分、提取字符串]、)cmd命令错误汇总,cmd错误
Deep understanding of distributed cache design
Fastdfs data migration operation record
Eventbus source code analysis
第五篇,STM32系统定时器和通用定时器编程
STM32开发资料链接分享
[100 cases of JVM tuning practice] 04 - Method area tuning practice (Part 1)
Pytorch中torch和torchvision的安装
【批处理DOS-CMD命令-汇总和小结】-查看或修改文件属性(ATTRIB),查看、修改文件关联类型(assoc、ftype)
Configuring the stub area of OSPF for Huawei devices
[HFCTF2020]BabyUpload session解析引擎
"Exquisite store manager" youth entrepreneurship incubation camp - the first phase of Shunde market has been successfully completed!
Mongodb client operation (mongorepository)
通过串口实现printf函数,中断实现串口数据接收
在jupyter中实现实时协同是一种什么体验
深度学习简史(一)
资产安全问题或制约加密行业发展 风控+合规成为平台破局关键
BFS realizes breadth first traversal of adjacency matrix (with examples)
Link sharing of STM32 development materials
代码克隆的优缺点