
Go Mutex
互斥锁是并发程序中对共享资源进行访问控制的主要手段,对此Go语言提供了非常简单易用的Mutex,Mutex为一结构体类型,对外暴露两个方法Lock()和Unlock()分别用于加锁和解锁。
Mutex 结构体
type Mutex struct {
state int32
sema uint32
}
Mutex.state
表示互斥锁的状态,比如是否被锁定等。Mutex.sema
表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。

Locked
: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。Woken
: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。Starving
:表示该Mutex是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过 1 ms。Waiter
: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
协程之间抢锁实际上是抢给Locked赋值的权利,能给Locked域置1,就说明抢锁成功。抢不到的话就阻塞等待Mutex.sema信号量,一旦持有锁的协程解锁,等待的协程会依次被唤醒。
加解锁过程
简单加锁
假定当前只有一个协程在加锁,没有其他协程干扰,那么过程如下图所示:
加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功。从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化。
加锁阻塞
假定加锁时,锁已被其他协程占用了,此时加锁过程如下图所示:
从上图可看到,当协程B对一个已被占用的锁再次加锁时,Waiter计数器增加了1,此时协程B将被阻塞,直到Locked值变为0后才会被唤醒。
简单解锁
假定解锁时,没有其他协程阻塞,此时解锁过程如下图所示:
由于没有其他协程阻塞等待加锁,所以此时解锁时只需要把Locked位置为0即可,不需要释放信号量。
解锁并唤醒协程
假定解锁时,有1个或多个协程阻塞,此时解锁过程如下图所示:
协程A解锁过程分为两个步骤,一是把Locked位置0,二是查看到Waiter>0,所以释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程B把Locked位置1,于是协程B获得锁。
自旋
加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。
自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
优势
自旋更充分的利用了CPU,避免了协程切换。因为当前申请加锁的协程获得了CPU,如果通过短时间自旋就可以获得锁,那么就可以直接运行,而不用阻塞并切换协程。
条件
- 运行在多 CPU 的机器上
- 当前 Goroutine 为了获取该锁进入自旋的次数小于四次
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空
- 互斥锁只有在普通模式才能进入自旋
func sync_runtime_canSpin(i int) bool {
// sync.Mutex is cooperative, so we are conservative with spinning.
// Spin only few times and only if running on a multicore machine and
// GOMAXPROCS>1 and there is at least one other running P and local runq is empty.
// As opposed to runtime mutex we don't do passive spinning here,
// because there can be work on global runq or on other Ps.
if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
return false
}
if p := getg().m.p.ptr(); !runqempty(p) {
return false
}
return true
}
问题
如果自旋过程中获得了锁,那么之前阻塞的协程就无法获得锁。如果等待加锁的协程特别多,而都在自旋过程中获得了锁,那么之前阻塞的协程就将一直阻塞。
为了解决这个问题,在1.8的版本之后增加了一个状态 Starving。在这个状态下不会自旋,一定会有一个协程被唤醒并加锁
Mutex 模式
Normal模式
默认情况下都是 Normal 模式
当一个协程加锁失败时,不会立即转入等待状态,而是判断是否满足自旋条件,如果满足,则自旋来等待锁
Starving模式
自旋模式抢到锁,表示有协程释放了锁,我们知道释放锁时,如果 waiter > 0,即有阻塞等待的协程,会释放信号量来唤醒协程,当协程被唤醒后,发现 Locked = 1,锁又被抢占,则又会阻塞,但在阻塞前会判断自上次阻塞到本次阻塞经历了多长时间,如果超过 1ms 的话,会将 Mutex 标记为"饥饿"模式,然后再阻塞。
在饥饿模式下,不会启动自旋,如果有协程释放锁,那么一定会唤醒一个协程,被唤醒的协程会获得锁,同时会把 waiter 减 1.
Woken状态
Woken 状态作用于加锁和解锁的过程中,如果一个协程正在解锁,另一个协程在自旋等待加锁,那么会把 Woken 状态置为1,通知解锁的协程不用释放信号量。