互斥锁
前言
本次的代码是基于go version go1.13.15 darwin/amd64
什么是sync.Mutex
sync.Mutex 是Go标准库中常用的一个排外锁。当一个goroutine 获得了这个锁的拥有权后, 其它请求锁的goroutine 就会阻塞在Lock 方法的调用上,直到锁被释放。
var (
mu sync.Mutex
balance int
)
func main() {
Deposit(1)
fmt.Println(Balance())
}
func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}
func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}
使用起来很简单,对需要锁定的资源,前面加Lock() 锁定,完成的时候加Unlock() 解锁就好了。
分析下源码
const (
重点开看下state 的几种状态:
大神写代码的思路就是惊奇,这里state 又运用到了位移的操作
-
mutexLocked 对应右边低位第一个bit 1 代表锁被占用 0代表锁空闲
-
mutexWoken 对应右边低位第二个bit 1 表示已唤醒 0表示未唤醒
-
mutexStarving 对应右边低位第三个bit 1 代表锁处于饥饿模式 0代表锁处于正常模式
-
mutexWaiterShift 值为3,根据 mutex.state >> mutexWaiterShift 得到当前阻塞的goroutine 数目,最多可以阻塞2^29 个goroutine 。
-
starvationThresholdNs 值为1e6纳秒,也就是1毫秒,当等待队列中队首goroutine 等待时间超过starvationThresholdNs 也就是1毫秒,mutex进入饥饿模式。
Lock
加锁基本上就这三种情况:
1、可直接获取锁,直接加锁,返回;
2、有冲突 首先自旋,如果其他goroutine 在这段时间内释放了该锁,直接获得该锁;如果没有就走到下面3;
3、有冲突,且已经过了自旋阶段,通过信号量进行阻塞;
-
1、刚被唤醒的 加入到等待队列首部;
-
2、新加入的 加入到等待队列的尾部。
4、有冲突,根据不同的模式做处理;
-
1、饥饿模式 获取锁
-
2、正常模式 唤醒,继续循环,回到2
梳理下流程
1、原子的(cas)来判断是否加锁,如果之前锁没有被使用,当前goroutine 获取锁,结束本次Lock 操作;
2、如果已经被别的goroutine 持有了,启动一个for循环去抢占锁;
会存在两种状态的切换 饥饿状态和正常状态
如果一个等待的goroutine有超过1ms(写死在代码中)都没获取到锁,那么就会把锁转变为饥饿模式
如果一个goroutine获取到了锁之后,它会判断以下两种情况:
-
1、它是队列中最后一个goroutine;
-
2、它拿到锁所花的时间小于1ms;
以上只要有一个成立,它就会把锁转变回正常模式。
3、如果锁已经被锁了,并且不是饥饿状态,并且满足自旋的条件,当前goroutine会不断的进行自旋,等待锁被释放;
4、不满足锁自旋的条件,然后结束自旋,这是当前锁的状态可能有下面几种情况:
-
1、锁还没有被释放,锁处于正常状态
-
2、锁还没有被释放, 锁处于饥饿状态
-
3、锁已经被释放, 锁处于正常状态
-
4、锁已经被释放, 锁处于饥饿状态
5、如果old.state 不是饥饿状态,新的goroutine 尝试去获锁,如果是饥饿状态,就直接将锁直接转给等待队列的第一个;
6、如果锁是被获取或饥饿状态,等待者的数量加一;
7、当本goroutine 被唤醒了,要么获得了锁,要么进入休眠;
8、如果old state 的状态是未被锁状态,并且锁不处于饥饿状态,那么当前goroutine 已经获取了锁的拥有权,结束Lock ;
9、判断一下当前goroutine 是新来的还是刚被唤醒的,新来的加入到等待队列的尾部,刚被唤醒的加入到等待队列的头部,然后通过信号量阻塞,直到当前goroutine 被唤醒;
10、判断如果当前state 是否是饥饿状态,不是的唤醒本次goroutine ,继续循环,是饥饿状态继续往下面走;
11、饥饿状态,当前goroutine 来设置锁,等待者减一,如果当前goroutine 是队列中最后一个goroutine 设置饥饿状态为正常,拿到锁结束Lock 。
位运算
上面有很多关于&和|的运算和判断,下面来具体的分析下
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
&^ 位清空(AND NOT)
<< 左移
>> 右移
&
参与运算的两数各对应的二进位相与,两个二进制位都为1时,结果才为1
0101
AND 0011
= 0001
|
参与运算的两数各对应的二进位相或,两个二进制位都为1时,结果才为0
0101(十进制5)
OR 0011(十进制3)
= 0111(十进制7)
^
按位异或运算,对等长二进制模式或二进制数的每一位执行逻辑异或操作。操作的结果是如果某位不同则该位为1,否则该位为0。
0101
XOR 0011
= 0110
&^
将运算符左边数据相异的位保留,相同位清零
0001 0100
&^ 0000 1111
= 0001 0000
<<
各二进位全部左移若干位,高位丢弃,低位补0
0001(十进制1)
<< 3(左移3位)
= 1000(十进制8)
>>
各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0
1010(十进制10)
>> 2(右移2位)
= 0010(十进制2)
Unlock
梳理下流程:
1、首先判断如果之前是锁的状态是未加锁,Unlock 将会触发panic ;
2、如果当前锁是正常模式,一个for循环,去不断尝试解锁;
3、饥饿模式下,通过信号量,唤醒在饥饿模式下面Lock 操作下队列中第一个goroutine 。
总结
1、加锁的过程会存在正常模式和互斥模式的转换;
2、饥饿模式就是保证锁的公平性,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时;
3、锁的状态的转换,也使用到了位运算;
4、一个已经锁定的互斥锁,允许其他协程进行解锁,不过只能被解锁一次;
参考
【sync.Mutex 源码分析】https://reading.hidevops.io/articles/sync/sync_mutex_source_code_analysis/ 【一份详细注释的go Mutex源码】http://cbsheng.github.io/posts/一份详细注释的go-mutex源码/ 【源码剖析 golang 中 sync.Mutex】https://www.purewhite.io/2019/03/28/golang-mutex-source/ 【sync.mutex 源代码分析】https://colobu.com/2018/12/18/dive-into-sync-mutex/ 【源码剖析 golang 中 sync.Mutex】https://www.purewhite.io/2019/03/28/golang-mutex-source/
来自:https://boilingfrog.github.io/2021/03/14/sync.Mutex/
|
请发表评论