愉快的生活从一篇文章开始,虽然Go的主题月已经结束了,我也如愿以偿拿到了我的瓷缸杯,但是,我还是要继续输出文章,可能这就是❤️ 吧
前言
今天的话,我想聊一下Go中对于Mutex的一个概念,从字面上我们可以看出,它就是一个? 的概念,而在Go中主要是goroutine并发的操作同一个资源单位的时候,使用Mutex,而今天我们也从Mutex的结构开始,一步步来了解Go中的Mutex。
正文
我们知道Mutex在Go中是被放在sync包里面的,其中它的结构也非常的简单,大概长这样
type Mutex struct {
state int32
sema uint32
}
复制代码
其中state置为0的时候为未加锁的状态,而为1的时候,则是加了锁的状态。
另一个参数则是信号量,它的作用主要是用来在争抢锁的失败之后排队用的(后面细说)。
抢锁的模式
在Go一共可以分为两种抢锁的模式,一种是正常模式,另外一种是饥饿模式
正常模式
在刚开始的时候,是处于正常模式,也就是,当一个G1持有着一个锁的时候,G2会自旋的去尝试获取这个锁
当自旋超过4次还没有能获取到锁的时候,这个G2就会被加入到获取锁的等待队列里面,而这个G在队列中的顺序就是根据信号量来决定的。
这时候,如果G1释放了这个锁,这时候,G2就会被从队列中被唤醒,然后去竞争这个锁,这时候,不只是有一个G2在对锁进行争抢,还会有其他正在自旋中的G也在争抢这把锁。
这时候,因为G2是被唤醒来去抢锁的,相对于在CPU中本身已经的自旋状态的G4和G5,它更不容易获得这个锁,那么这时候,如果它获取锁失败的时候,G2就会被重新加入到等待队列的头部,继续下一轮的等待。
但是很明显,这样是不行的,这样会造成队列中的G长时间得不到执行,所以这时候就有了饥饿模式。
饥饿模式
当处于等待队列中的Goroutine超过1ms没有获取到锁的时候,它的状态就会变成饥饿模式,这个时候,如果G1把锁给释放出来的时候,G2是可以直接获取到这个锁的,而不用通过去争抢的方式,同时,后续的G4和G5也不会进入自旋的状态,而是直接的就进入到了等待队列后面去了
那么也不可能说永远的保持一个饥饿的状态,总归会有吃饱的时候,也就是总有那么一刻Mutex会回归到正常模式,那么回归正常模式必须具备的条件有以下几种
1.G2的执行时间小于1ms
2.处于等待队列已经全部清空了
当满足上述两个条件的任意一个的时候,Mutex会切换回正常模式,而Go的抢锁的过程,就是在这个正常模式和饥饿模式中来回切换进行的。
在正常模式下,程序会让Goroutine有自旋的这一个状态,是因为为了避免频繁的唤起和睡眠Goroutine的过程中带来的大的开销,通过自旋+队列排队的方式,来增加更大的一个吞吐量,但是这种方式,很容易会出现一个尾端延迟执行的情况。
而在饥饿模式下,则会把Goroutine加入到队列中去,严格的按照顺序执行,这样就不会带来尾端延迟的问题,但相对应的,程序的吞吐量也就没有正常模式下来的高了。
Goroutine的自旋
前面我们提到了,在正常模式下,新的Goroutine是会进入一个自旋的状态去抢锁的,那么是不是只要是新的Goroutine都会进入一个自旋的状态呢,其实也不是,想要进入自旋的状态必须满足下面的几个条件。
-
必须是一个多核的机子
-
gomaxproces必须大于1
-
除了正在运行的P以外,还必须要有其他的P也在处于running的状态
-
当前run P的本地队列为空
只有同时满足了这四个条件,这个Goroutine才会进入一个自旋的状态。
最后
最近在看Go中的关于锁的一些源码,还有一些小视频,在这里做一些总结,如果你觉得这篇文章对你有所帮助,点个赞再走呀!!!