Go互斥锁加锁逻辑中的的两种运行模式
提前给结论
一开始,互斥锁是处于正常模式。 在加锁的时候,如果没有成功,则开始自旋。 如果自旋4次后还没有拿到锁,则改协程会被加入到一个FIFO队列中,并阻塞等待唤醒 正常模式下,被唤醒的协程不会直接拥有锁,而是会和新请求锁的协程竞争锁。 饥饿模式下,直接获得锁,不用和新的协程竞争锁。
Go中的互斥锁
Go中互斥锁,就是一个sync.Mutex类型。其特点是只能存在一个写者或读者,不能同时读和写。
sync.Mutex代码位于sync/mutex.go中
结构体定义
type Mutex struct {
state int32
sema uint32
}
其中state标识状态,sema是信号量。
state第一个bit标识是否锁定,mutexLocked
第二个bit位标识是否被协程唤醒,mutexWoken
第三个bit位标识是否是饥饿模式,mutexStarving
上锁
先看一下上锁的逻辑:
func (m *Mutex) Lock() {
// 对state 进行原子操作 本质是加锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 如果加锁不成功 调用lockSlow
m.lockSlow()
}
在lockSlow方法中
// 如果已经被锁定 但是不是饥饿模式 并且可以自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
//***
runtime_doSpin() // 自旋等待获取锁
//***
}
// starvationThresholdNs就是1ms 如果超过1ms没有获取到锁,则进入饥饿模式
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
综上,加锁的流程如下:
是否能自选的判断:
func sync_runtime_canSpin(i int) bool {
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
}
通过代码我们可以分析出,能自旋的条件有:
- 自旋次数小于4
- cpu大于1个
- 有空闲的P
- 已经被锁定并且不是饥饿模式(本条通过lockSlow得出)
解锁
再看一下解锁的逻辑
func (m *Mutex) Unlock() {
//先尝试直接解锁
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// 如果有别的协程等待 需要唤醒等待的协程
m.unlockSlow(new)
}
}
unlockSlow的实现
func (m *Mutex) unlockSlow(new int32) {
// ***
// 如果是饥饿模式
if new&mutexStarving == 0 {
old := new
for {
// 如果没有需要唤醒的协程 或者一个协程已经拿到锁 则不进行解锁
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 解锁 然后唤醒别的协程
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
// 如果不是饥饿模式 直接唤醒阻塞的协程
runtime_Semrelease(&m.sema, true, 1)
}
}
综上,解锁的流程如下:
同时需要注意:
- 不能在Lock之前执行Unlock 会panic
- 互斥锁不支持重入,Lock之后再Lock会死锁
- 互斥锁跟协程不关联,可以由一个协程Lock 再由另一个协程UnLock