Sync.Mutex
Mutex结构
type Mutex struct {
state int32
sema uint32
}
Sync.Mutex由两个字段构成,state
用来表示当前互斥锁处于的状态,sema
用于控制锁状态的信号量
互斥锁state(32bit)主要记录了如下四种状态:
- waiter_num(29bit):记录了当前等待这个锁的goroutine数量
- starving(1bit):当前锁是否处于饥饿状态,
0: 正常状态 1: 饥饿状态
- woken(1bit):当前锁是否有goroutine已被唤醒。
0:没有goroutine被唤醒; 1: 有goroutine正在加锁过程
Woken 状态用于加锁和解锁过程的通信,例如,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把 Woken 标记为 1,用于通知解锁协程不必释放信号量了。 - locked(1bit):当前锁是否被goroutine持有。
0: 未被持有 1: 已被持有
sema信号量的作用:当持有锁的gorouine释放锁后,会释放sema信号量,这个信号量会唤醒之前抢锁阻塞的gorouine来获取锁。
锁的两种模式
互斥锁主要有两种模式: 正常模式和饥饿模式
之所以引入了饥饿模式,是为了保证goroutine获取互斥锁的公平性。公平性是指多个goroutine在获取锁时,goroutine获取锁的顺序,和请求锁的顺序一致。
正常模式下,所有阻塞在等待队列中的goroutine会按顺序进行锁获取,当唤醒一个等待队列中的goroutine时,此goroutine并不会直接获取到锁,而是会和新请求锁的goroutine竞争。通常新请求锁的goroutine更容易获取锁,这是因为新请求锁的goroutine正在占用cpu片执行,大概率可以直接执行到获取到锁的逻辑。
这样就会出现阻塞队列中的goroutine一直在等待
的情况,因此引入了饥饿模式。
饥饿模式下, 新请求锁的goroutine不会进行直接进行锁获取,而是加入到队列尾部阻塞等待获取锁。
饥饿模式的触发条件
:
- 当一个goroutine等待锁的时间超过1ms时,互斥锁会切换到饥饿模式
自旋过程抢到锁,意味着同一时刻有协程释放了锁,释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到 CPU 后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过 1ms 的话,会将 Mutex 标记为”饥饿”模式,然后再阻塞。
饥饿模式的取消条件
:
- 当获取到锁的这个goroutine是等待锁队列中的最后一个goroutine,互斥锁会切换到正常模式;
- 当获取到锁的这个goroutine的等待时间在1ms之内,互斥锁会切换到正常模式
自旋
自旋过程是指,goroutine尝试加锁时,如果当前 Locked 位为 1,则说明该锁当前是由其他协程持有,尝试加锁的协程并不会马上转入阻塞队列,而是会持续的探测 Locked 位是否变为 0
。自旋的时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞,这个可不是我们想要的。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换
。但是自旋会导致阻塞队列中的goroutine一直获取不到锁,处于饥饿状态
。
自旋必须满足以下所有条件:
- 自旋的次数要足够小,通常为4,即「自旋最多为4次」
- CPU 核数要大于1,否则自旋是没有意义的,因为此时不可能有其他协程释放锁
- 协程调度机制中的 Process 数量要大于 1,比如使用 GOMAXPROCS() 将处理器设置为 1 就不能启用自旋
- 协程调度机制中的可运行队列必须为空,否则会延迟协程调度
为了避免协程长时间无法获取锁,自1.8版本以来增加了一个状态,即 Mutex 的 Starving
状态。这个状态下不会自旋,而是直接进入阻塞队列,一旦有协程释放锁,那么一定会唤醒一个阻塞队列中的协程并成功加锁。