接上面的锁来聊一聊go的mutex
Golang mutex
mutex的由来Mutual exclusion(互斥)的前缀组合,俗称互斥体/互斥锁
mutex的数据结构
因为足够简单,因此不需要额外初始化,这个结构体的零值就是一个有效的互斥锁,处于unlocked状态。
state存储互斥锁的状态,加锁和解锁都是通过atomic包提供的函数原子性操作该字段
sema用作信号量,主要用做等待队列。
mutex的两种模式
正常模式
在正常情况下,尝试加锁的goroutine会先自旋,通过原子操作获得锁。
如果几次自旋之后仍然获取不到锁,则通过信号量等待
所有的等待者都会按照先入先出的顺序排队等待
当锁被释放,第一个等待者并不会直接拥有锁,而是会和那些处于自旋状态,尚未排队等待的goroutine,这种情况下,正在自旋的goroutine更有优势,原因如下:
- 处于自旋状态的goroutine本身就正在cpu上面运行
- 自旋的goroutine有很多个而被唤醒的只有一个,因此被唤醒的goroutine很大概率上拿不到锁
这种情况下这个goroutine会被重新插入到队列的头部,而不是尾部。
饥饿模式
当一个goroutine本次加锁等待时间超过了1ms之后,它会把当前的Mutex从正常模式转化为饥饿模式,在饥饿模式下,mutex的所有权,从执行Unlock的goroutine转换到等待队列头部的Goroutine,后来者也不会自旋,也不会尝试获取锁,会直接到等待队列的尾部。
饥饿模式转换为正常模式
当一个等待着获得锁之后,它会在以下两种情况时将mutex从饥饿模式切换为正常模式
- 它的等待时间小于1ms
- 它是最后一个等待着
总结
正常模式,自旋和排队是同时存在的。
- 优点
这样可以保证良好的吞吐量,因为频繁的挂起、唤醒goroutine会带来较多的开销,但是又不能无限制的自旋,要把自旋控制在一个较小的范围。
- 缺点
可能会出现队列尾端的goroutine迟迟抢不到锁(尾端延迟的情况)
饥饿模式下,不再自旋尝试,所有goroutine统统都要排队
- 优点
可以减少尾端延迟
- 缺点
牺牲了吞吐性
lock和unlock的逻辑
state的类型时int32
第一位用作锁状态标识,置为1,表示已加锁,对应掩码常量位mutexLocked
第二位标记是否已经唤醒了,置为1,表示已经唤醒,对应掩码常量为mutexWoken
第三位标记mutex的工作模式,置为1,表示饥饿模式,对应掩码常量为mutextStaring
mutexWaiterShift=3,表示除了最低三位意外,state的其他位用来记录有多少个等待者在排队
lock和unlock方法
下面是精简掉了注释和race检测相关的代码
两个方法中主要通过atomic函数实现了fast path,相应的slowpath被单独放在了lockslow和unlockSlow方法中
源码注视中说:这样是为了便于编译器对内联fastpath进行优化
lock
lock方法的fastpath希望mutwx处于unlocked状态,没有goroutine在排队,更不会饥饿
理想状态下一个cas操作就可以获取锁
如果一个cas操作没有获取锁,就需要执行lockslow方法
unlock
unlock方法类似,需要用原子操作从mutex.state中减去mutexLocked(释放锁)
然后根据state的新值,来判断是否需要执行slowpath
如果新值为0,也就代表没有giroutine在排队,也就不需要执行其他操作,如果新值为0,就执行unlockslow,看看是不是需要唤醒某个giroutine