Skip to main content

互斥锁

请介绍实现原理

Mutex数据结构只有两个字段,分别是:state、sema

type Mutex struct {
state int32 // 表示锁状态:1:加锁状态、2:唤醒状态、4:饥饿状态
sema uint32 // 表示用来控制等待者队列的信号量
}

一次加锁的过程为:

  1. 使用原子锁给自己的state字段进行加锁。如果加锁成功则直接返回,如果失败则继续执行后面步骤。
  2. 判断自旋条件,并尝试自旋申请锁。如果加锁成功则直接返回,如果自旋失败,则继续执行后面步骤。
  3. 阻塞等待信号通知。

什么是正常状态和饥饿状态

在Go中,互斥锁有两种操作状态:正常状态和饥饿状态。

在正常状态下,互斥锁等待者会按照先进先出的顺序排队等待唤醒。但是,对于被唤醒的协程,并不意味着拥有互斥锁。 因为对一些新创建的协程来说,他们已经处于运行状态,因此他们有更大的概率获取到互斥锁。 而对于刚被唤醒的协程来说,他们一旦申请锁失败,会重新进入队头等待唤醒。 这时,如果等待者超过1ms仍旧无法获取到互斥锁,那么它会将互斥锁从正常状态切换到饥饿状态。

在饥饿状态下,互斥锁所有者在释放锁后,会直接将锁权限移交给等待队列头部的协程。 对于新创建的协程来说,它不会自旋尝试获取锁,而是会直接进入到等待队列尾部。 对于等待队列的协程来说,如果他是队列中最后一个等待者,或者获取锁的时间少于1ms,那么它会将互斥锁从饥饿状态切换到正常状态。

在正常模式下,互斥锁会有更好的性能,因为它支持一定条件下的自旋去获取锁。 而饥饿模式又可能有效的避免协程饿死的情况。

允许自旋的条件

当Mutex使用原子锁进行加锁失败时,在满足自旋的条件下会尝试进行自旋加锁。允许自旋的条件有:

  • 处于饥饿状态下,放弃自旋
  • 当自旋次数iter大于等于4时,放弃自旋