之前发布的 sync mutex golang主要是从场景触发,该篇主要是代码出发
总结
关键字:cas、semaphore(信号量)、自旋、饥饿状态
- Mutex 全称是mutual exclusion互斥,是golang的互斥锁的实现。golang的互斥锁,主要是使用cas和信号量来实现互斥,cas操作状态量-state,acquire和release操作信号量-sema。
- 进入Lock wait状态,一定是有一个goroutine已经进入了Lock,当前goroutine再进入才会被挂起。
- 大致过程为第一次调用Lock,会直接通过cas操作将state设置为mutexLocked状态,如果这时再有goroutine来进行Lock,则会进行自旋,如果满足的自旋条件的话,自旋完成后其它goroutine还未进行Unlock,则当前goroutine会尝试获取信号量,这时因为获取不到信号量,当前goroutine会被挂起阻塞,如果这时有之前的goroutine执行Unlock操作,则会release 信号量,并将阻塞的goroutine唤醒,执行完Lock函数,继续执行业务代码,执行完业务代码,Unlock锁,完成整个加解锁过程。
关键字说明
cas
compare and swap,用作设置state状态
semaphore
- 获取过程为将sema减一的过程,如果sema小于0,则将当前goroutine挂起
- 释放过程为将sema加一的过程,并将在获取信号量时被挂起的goroutine唤醒。
- 这篇文章不会讲述信号量具体实现,后面会规划单独一篇进行讲述。
自旋
为了减少goroutine的挂起、唤醒,先进行自旋,自旋不会将goroutine挂起来,只是进行cpu空转,后面会讲什么条件可以进入自旋
饥饿状态
为了防止有些waiter一直在等待,加入了饥饿状态。如果goroutine等待信号量超过1ms,会进入饥饿状态,下回Unlock唤醒,会直接将下一个waiter直接唤醒,具体在Unlock里面实现
代码注释说明
代码目录
代码入口在sync/mutex.go里面,struct为Mutex,并且定义了Locker的接口,接口为Lock和Unlock函数。Mutex实现了Locker接口。
结构体
type Mutex struct {
state int32 // 存储状态码和当前的waiter个数
sema uint32 // 信号量
}
状态码
状态码使用int32表示,低三位分别表示三个状态,三位以上表示当前的waiter个数
状态码的定义很巧妙的使用的iota
mutexLocked = 1 << iota // 被锁状态
mutexWoken //2 被锁状态,获取到信号量,当前goroutine会启动,进行执行代码
mutexStarving //4 饥饿状态
mutexWaiterShift = iota // 3位以上记录waiter个数
图结构表示如下
代码走读
Lock实现
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex. 判断当前state是否为0,0为未上锁状态,通过cas操作实现,如果当前未Lock过直接设置为mutexLocked状态
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// Slow path (outlined so that the fast path can be inlined) 当前未加锁成功,则进入慢加锁操作,对应解锁有unlockSlow
m.lockSlow()
}
过程:进入该函数,都会先判断是否可以自旋,如果可以自旋,则先进行自旋,自旋完成如果还没有进入Unlock,则尝试获取信号量,默认sema等于0,会获取不到信号量,因而当前goroutine1被挂起
如果这时有其它goroutine0进行unlock,则会获取信号量成功(获取信号量,是按照队列的方式实现,先进先出,保证公平性)。获取信号量成功后,会进入mutexWakeup状态,并且这个状态会在循环的下一轮设置。进入下一轮for循环,会判断当前锁状态(old),如果状态是被解锁状态,则直接return,从而当前goroutine1完成加锁,继续执行业务代码。
func (m *Mutex) lockSlow() {
var waitStartTime int64 // 等待时间
starving := false // 是否在饥饿状态
awoke := false // 是否被唤醒
iter := 0 // 自旋的次数
old := m.state // 当前状态,整个过程存在old和new两个state量,old是当前循环state的状态,new为要被设置成的state
for {
// old&(11) 当前为mutexLocked状态 并且能自旋,则进入自旋状态,如果是starving状态,直接跳过
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
}
// 进入自旋,实现为调用PAUSE指令,代码实现的地方在asm_amd64.s(这里是amd64架构为例)
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 增加waiter数
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.
// starving是循环的上一轮结果赋值的,是在获取信号量后,计算挂起的时间超过1ms,会被设置为starving=true
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// awoke也是循环的上一轮结果赋值的,在获取到信号量后
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状态,跟old一样时,进入下面case,该出设置state
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 出口,退出lock,执行业务代码
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)
// 计算是否超过1ms,是否设置为饥饿状态
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 {
// 如果其他goroutine已经修改state状态,重新进入for循环的下一轮
old = m.state
}
}
}
自旋
说明:自旋是执行PAUSE指令,让cpu空转,不会将当前goroutine挂起
runtime_canSpin具体实现在runtime/pro.go里面的sync_runtime_canSpin。
mutex是并发式的,从而对于自旋的处理的会相对谨慎,只有满足下面的场景,才会进入自旋场景
- 自旋次数限制
- 是否多核
- p的本地q是否有goroutine
返回false的场景: - i超过4,即最多调用4次
- cpu数为1个
- gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1
- 当前p的本地队列有goroutine
返回true的场景:
其它场景返回都返回true
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
}
runtime_doSpin的具体实现
在runtime/proc.go里的sync_runtime_doSpin,里面调用procyield(active_spin_cnt),自旋次数为30次,具体实现为汇编代码,以amd64架构为例,
在asm_amd64.s里面,实际为调用PAUSE,即让cpu进行空转,不让出cpu调用
TEXT runtime·procyield(SB),NOSPLIT,$0-0
MOVL cycles+0(FP), AX
again:
PAUSE
SUBL $1, AX
JNZ again
RET
Unlock解锁
func (m *Mutex) Unlock() {
// Fast path: drop lock bit.
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Outlined slow path to allow inlining the fast path.
// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
m.unlockSlow(new)
}
// 如果当前只有一个lock了,则直接返回
}
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
old := new
for {
// 当前有多个waiter,但是未进入饥饿状态
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 饥饿状态下:让下一个waiter先被调度到,释放出时间片,从而下一个waiter开始后能马上执行
// Starving mode: handoff mutex ownership to the next waiter, and yield
// our time slice so that the next waiter can start to run immediately.
// Note: mutexLocked is not set, the waiter will set it after wakeup.
// But mutex is still considered locked if mutexStarving is set,
// so new coming goroutines won't acquire it.
runtime_Semrelease(&m.sema, true, 1)
// 释放信号量,实则让sema+1,从而waiter能acquire信号量
}
}
感想
golang的互斥锁实现很精致,涉及的点比较多,有cas、信号量、自旋、饥饿等,使用32位的int32表示lock、awake、starving和waiter个数很独特(个人而言,类似实现在go源码中很多,如WaitGroup等)。当前阅读源码,只是了解大致过程,对与细小的点还得后续继续阅读源码。