基本知识
获取锁的方案
对于获取锁,一般来讲有两种方案,一种是不断地自旋+CAS,另一种就是阻塞+唤醒。两种方式各有优劣。Go语言结合了这两种方案,自动的判断当前锁的竞争情况,先尝试自旋几次,如果锁一直没被释放,再加入阻塞队列。
公平性
Go语言的锁存在两种模式:正常模式和饥饿模式。
正常模式:阻塞的协程进入FIFO的阻塞队列,所有协程对锁进行抢占式调度。也就是说,当某个协程从阻塞队列被唤醒时,它并不能直接获得锁,而是还要自己再去抢占一次(此时可能有其他协程刚刚进入Lock()流程,还在自旋尝试,没有进入阻塞队列)。源码的注释中提到,在这种模式下,从阻塞队列被唤醒的协程处于劣势。因为新来的协程已经在占用CPU,并且可能数量很多。
饥饿模式:上面提到从阻塞队列唤醒的协程在竞争上处于劣势,有可能某协程一直在阻塞队列,得不到锁。为了避免这种情况,当某协程在阻塞队列阻塞了很长一段时间(1ms)后,会将锁设置为饥饿模式。在饥饿模式下,想要获得锁的所有协程不会自旋等待,而是直接进入阻塞队列尾部去排队。
Mutex结构体
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6 //1ms
)
state表示了该锁的所有信息,包括mutexLocked(是否已上锁),mutexWoken(是否有在运行的协程在尝试抢占锁,便于Unlock的时候判断是否要从阻塞队列唤醒协程), mutexStarving(是否处于饥饿模式),以及在阻塞队列的协程数量。
state的bit布局如下:
|31-----------------3|-------2-----|-----1----|-----0-----|
|阻塞队列中的协程数量|mutexStarving|mutexWoken|mutexLocked|
Lock()
下面来看一下Lock方法具体的实现。
func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
......
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
第一个if条件表示,如果锁是一把全新(没有其他协程动过)的锁,直接锁上就行了,然后返回。否则进入lockSlow()流程。
lockSlow()
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
//...
}
局部变量的意义:
waitStartTime:在阻塞队列中等待的总时长,这个值是会累加的。
starving:标记是否要将锁改为饥饿模式。
awoke:标记是否要修改锁的mutexWoken位。
iter:尝试自旋的次数。
old:锁的老状态,用于CAS操作等。
for {
// Don't spin in starvation mode, ownership is handed off to waiters
// so we won't be able to acquire the mutex anyway.
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
// Try to set mutexWoken flag to inform Unlock
// to not wake other blocked goroutines.
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
//...
}
1、先判断锁的状态,要锁已经被占用、不为饥饿模式(饥饿模式不能抢占,要去阻塞队列排队)、能继续自旋才进入第一个if的逻辑。
2、假如当前锁mutexWoken为0,即没有其他协程在运行并等待,并且阻塞队列的大小不为0,就可以将mutexWoken标记为1。
for{
//...
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
if old&mutexStarving == 0 {
new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
//...
}
这里的逻辑为自旋结束,构造一个新的state,准备获取锁了。
1、如果old的mutexStarving为0,说明当前协程可以加锁,在new中写入mutexLocked位为1。
2、如果old的mutexLocked或者mutexStarving为1,当前协程肯定不能加锁,阻塞队列的值加1。
3、如果当前协程为starving状态(在阻塞队列中等了太长时间)并且old的状态为锁定,将new的mutexStarving位写为1。
4、如果当前协程为awoke状态,需要修改new的mutexWoken位的值,写为0(后续当前线程只有获取锁和阻塞两种可能)。
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
//被唤醒
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode.
// Critical to do it here and consider wait time.
// Starvation mode is so inefficient, that two goroutines
// can go lock-step infinitely once they switch mutex
// to starvation mode.
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
这里为尝试更新锁状态,如果CAS操作失败,则更新old的值后继续循环。
如果成功:
1、如果old未被锁定且不为饥饿模式,说明当前协程加锁成功,函数可以返回。
2、判断当前协程是刚来还是从阻塞队列中醒来,如果是刚来,放到阻塞队列末尾,否则放到阻塞队列头部。
3、runtime_SemacquireMutex(&m.sema, queueLifo, 1)表示放入阻塞队列并阻塞。
4、被唤醒后更新starving的值。
5、如果当前锁是饥饿模式,则当前协程可以直接获取锁, 否则又回到循环,尝试抢占。
6、获取锁时要更新mutexLocked位、阻塞队列的大小。如果当前协程不在starving状态或者阻塞队列中只有一个协程,将锁改为正常模式。
Unlock()
比较简单,暂时省略。