首页 > 编程知识 正文

同步信号量和互斥信号量,golang锁机制

时间:2023-05-05 01:45:36 阅读:110702 作者:3132

互斥锁sync.Mutex前言Mutex摘要Mutex.state状态idmutex源代码分析

前言

Golang互斥锁sync.Mutex

多元素概述

由于Mutex(mutualexclusion )、Go中的mutex的数据结构是这样的,十分简单,所以不需要额外的初始化,零值是有效的互斥锁,处于Unlocked状态。 state包含互斥锁的状态,锁定和解锁都取决于atomic包中提供的函数的原子性来操作此字段。 sema用作信号量,主要用于排队。

Mutex有两种模式。 在正常模式下,尝试锁定的goroutine首先旋转4次。 自旋锁(如果不行的话我会一直尝试),试图通过原子操作获得锁。 如果旋转了几次也没能获得摇滚的话,在信号量上等待。

所有等待者都按照先入先出的顺序排列。

但是,一旦解锁,第一个等待者清醒后,就需要与后来者竞争,而不是直接拿着锁。 也就是说,这是处于旋转阶段,还没有排队等待的goroutine。 在这种情况下,后来的人更有利。 另一方面,因为在CPU上运行,所以当然比刚清醒的goroutine更有利。 另一方面,虽然有很多处于自旋状态的goroutine,但是一次只有一个被唤醒的goroutine,所以不会锁定被唤醒的goroutine。 在这种情况下,它将重新插入队列的头部而不是尾部。

另一方面,如果这次的解锁等待时间超过1ms,goroutine会将当前的Mutex从通常模式切换到“饥饿模式”。

在饥饿模式下,Mutex的所有权从执行Unlock的goroutine直接传递到队列开头的goroutine,即使Mutex处于Unlocked的状态,后者也不会旋转,也不会试图获得锁。 直接排在队列末尾等待。

当等待者获得锁时,在以下两种情况下将Mutex从饥饿模式切换到正常模式:

第一种情况是等待时间小于1ms。 也就是说,来了不久。 第二种情况是,它是最后的等待者,等待队列空着,后面没有饥饿的goroutine

如上所述,正常模式下自旋和队列同时存在,执行lock的goroutine需要先自旋,如果尝试了4次仍未锁定,则需要去队列中等待。 这种排队前让大家抢的模式可以有更高的吞吐量。 由于频繁挂起,调用goroutine会带来很多开销。 然而,自旋不能无限制,必须减小自旋的开销,因此在正常模式下Mutex具有更好的性能。 但是,队列末尾的goroutine可能会延迟而无法夺取锁定(末尾的延迟)。

另一方面,饥饿模式不再尝试自旋,所有goroutine并列,严格的FIFO对于防止尾端延迟尤为重要。

Mutex.state状态标志

首先,我们来看一下关于Mutex.state的几个常数定义。 state为int32类型,其中第一个位为锁定状态标记,1表示已锁定,相应的掩码常量为mutexLocked,第二个位为记录goroutine是否已被唤醒,1表示被唤醒0表示正常模式,1表示饥饿模式,对应的掩码常数为mutexStartving,常数mutexWaiterShift为3,低位3位以外的state的其他位用于记录有多少人的队列。

让我们看看lock和unlock的方法。 注释和race检测相关代码的一部分被简化了。 在两种方法中,Fast path主要是通过atomic函数实现的。 相应的Slow path分别位于lockSlow和unlockSlow方法中。 根据源注释,这是为了便于编译器装载Fast path进行内联优化。

Lock的Fast path期待Mutex处于Unlocked状态,goroutine没有排队,不会饿。 理想情况下,一个CAS操作可以获得锁,但如果CAS操作没有获得锁,则必须进入Slow path,即lockSlow方法。

Unlock方法也同样,首先通过原子操作从state中减去mutexLocked,即解除锁定,根据state的新值判断是否需要执行Slow path。 如果新值为0,则不需要执行其他操作,因为其他goroutine将不会对齐。 如果新值不为0,则需要进入slow path以确认是否需要调用某个goroutine。 lock和unlock的fast path就是这样,然后展开slow path的主要逻辑。

Mutex源代码分析

如果某个goroutine尝试锁定mutex,但其他goroutine锁定后仍未释放,且当前mutex正在正常模式下运行,则会开始旋转,不是吗?

不,对于当前单核场景,旋转器的goroutine正在等待具有锁的goroutine解锁,而具有锁的goroutine正在等待旋转器的goroutine让出CPU。 在这种情况下,自旋没有意义。 此外,如果GOMAXPROCS=1,或者当前没有其他正在运行的p

,也和单核场景类似,同样不需要自旋。除此之外,如果当前P的本地runq不为空,相较于自旋而言,切换到本地runq更有效率,所以为保障吞吐量也不会自旋。

最终,只有在多核场景下,且GOMAXPROCS>1,至少有一个其他的P处于running,当前P的本地runq为空的情况下,才可以自旋。

进入自旋的goroutine会先去争抢mutex的唤醒标识位(自旋G与等待队列第一个G就是在此竞争),设置mutexWoken标识位的目的是,在正常模式下,告知持有锁的goroutine,在unlock的时候不用再唤醒其他goroutine了,已经有goroutine在这里等待,以免唤醒太多的等待协程。mutex中的自旋,底层是通过procyield循环执行30次PAUSE,自旋次数上限为4,而且每自旋一次都要重新判断是否可以继续自旋。

如果锁被释放了,或者锁进入了饥饿模式,亦或者已经自旋了4次,都会结束自旋。结束自旋或者根本不用自旋的goroutine,就该尝试原子操作修改mutex的状态了。把此时mutex.state保存了old中,把要修改为的新state记为new。

如果old处于饥饿模式或者加锁状态,goroutine就得去排队,所以在这些情况下排队规模要加1.如果是正常模式,就要尝试设置lock位,所以最后一位要置为1如果当前goroutine等待的时间已经超过1ms,而且锁还没被释放,就要将mutex的状态切换为饥饿模式,这里之所有还要求锁没被释放,是因为如果锁已经释放了,那怎么都得去抢一次啊,要是直接进入饥饿模式就只能去排队了。在执行原子操作修改state之前,若是当前goroutine持有唤醒标识的话,还要将唤醒标识位重置。

把排队规模和几个标志位都设置好以后,在执行原子操作修改state之前,若是当前goroutine持有唤醒标识的话,还要将唤醒标识位重置。因为接下来无论是要去抢锁,还是单纯的去排队,如果原子操作成功了,那么要么是成功抢到了锁,要么是成功进到了等待队列里。当前goroutine都不再是被唤醒的goroutine了,所以要释放唤醒标识。

而如果原子操作不成功,也就意味着其他goroutine在我们保存mutex.state到old以后,又修改了state的值,当前goroutine就要回过头去,继续从自旋检测这里开始再次尝试。所以也需要释放自己之前抢到的唤醒标识位,从头再来。

继续展开这个原子操作成功的分支,如果是抢锁操作成功了,那么加锁的slow path就可以宣告结束了。

如果是排队的规模设置成功了,还要决定是排在等待队列头部还是尾部,如果当前goroutine已经排过队,是在unlock中唤醒的,那就排在等待队列头部。

如果是第一次排队,就排到最后,并且从第一次排队开始记录当前goroutine的等待时间。

接下来就会让出,进到等待队列里面了(runtime_SemacquireMutex(&m.sema, queueLifo, 1)),队列里的goroutine被唤醒时(unlock),要从上次让出的地方(39行,runtime_SemacquireMutex的下面)开始继续执行,接下来会判断,如果mutex处在正常模式,那就接着从自旋开始抢锁,如果唤醒后的mutex处在饥饿模式,那就没有其他goroutine会和自己抢了,锁已经轮到自己这里,只需要把mutex.state中lock标识位设置为加锁,把等待队列规模减去1,再看看是不是要切换到正常模式,也就是自己的等待时间是不是小于1ms,或者等待队列已经空了,最后设置好mutex.state就一切ok了,这就是加锁操作的slow path。

接下来看unlock的slow path,进到unlock的slow path,说明除去lock标识为以外,剩下的位不全为0,如果处在正常模式,若等待队列为空,或已经有goroutine被唤醒或者获得了锁,或者锁进入了饥饿模式,那就不需要唤醒某个goroutine了,直接返回即可。否则就要尝试抢占mutexWoken标识位,获取唤醒一个goroutine的权利,抢占成功后,就会通过runtime_Semrelease函数唤醒一个goroutine,如果抢占不成功就进行循环尝试,直到等待队列为空,或已经有goroutine被唤醒或者获得了锁,或者锁进入了饥饿模式,则退出循环,而在饥饿模式下,后来的goroutine不会争抢锁,而是直接排队,锁的所有权是直接从执行unlock的goroutine,传递给等待队列中首个等待者的,所以不用抢占mutexWoken标识位。

第一个等待者唤醒后,会继承当前goroutine的时间片立刻开始运行,也就是继续lockSlow中这里,goroutine被唤醒以后的逻辑,这就是unlock的slow path。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。