一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
Mutex
MutexLock()Unlock()
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
)
复制代码
- Mutex 是一个互斥锁,其零值对应了未上锁的状态,不能被拷贝;
- state 代表互斥锁的状态,比如是否被锁定;
- sema 表示信号量,协程阻塞会等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程。
注意到 state 是一个 int32 变量,内部实现时把该变量分成四份,用于记录 Mutex 的状态。
- Locked: 表示该 Mutex 是否已经被锁定,0表示没有锁定,1表示已经被锁定;
- Woken: 表示是否有协程已经被唤醒,0表示没有协程唤醒,1表示已经有协程唤醒,正在加锁过程中;
- Starving: 表示该 Mutex 是否处于饥饿状态,0表示没有饥饿,1表示饥饿状态,说明有协程阻塞了超过1ms;
上面三个表示了 Mutex 的三个状态:锁定 - 唤醒 - 饥饿。
Waiter 信息虽然也存在 state 中,其实并不代表状态。它表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
LockedLockedsema
WokenStarving
Lock
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
复制代码
若当前锁已经被使用,请求 Lock() 的 goroutine 会阻塞,直到锁可用为止。
单协程加锁
Lockedatomic.CompareAndSwapInt32
加锁被阻塞
假设协程B在尝试加锁前,已经有一个协程A获取到了锁,此时的状态为:
此时协程B尝试加锁,被阻塞,Mutex 的状态为:
Locked
Unlock
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 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)
}
}
复制代码
Unlock
从源码注释来看,一个 Mutex 并不会与某个特定的 goroutine 绑定,理论上讲用一个 goroutine 加锁,另一个 goroutine 解锁也是允许的,不过为了代码可维护性,一般还是建议不要这么搞。
A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it.
无协程阻塞下的解锁
Locked
解锁并唤醒协程
假定解锁时有1个或多个协程阻塞,解锁过程分为两个步骤:
LockedWaiterLocked
自旋
LockedLocked
自旋的时间很短,如果在自旋过程中发现锁已经被释放,那么协程可以立即获取锁。此时即便有协程被唤醒,也无法获取锁,只能再次阻塞。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免一部分协程的切换。
什么是自旋
PAUSEsleepLockedPAUSEsleep
自旋条件
加锁时 Golang 的 runtime 会自动判断是否可以自旋,无限制的自旋将给 CPU 带来巨大压力,自旋必须满足以下所有条件:
GOMAXPROCS()
可见自旋的条件是很苛刻的,简单说就是不忙的时候才会启用自旋。
自旋的优势
自旋的优势是更充分地利用 CPU,尽量避免协程切换。因为当前申请加锁的协程拥有 CPU,如果经过短时间的自旋可以获得锁,则当前写成可以继续运行,不必进入阻塞状态。
自旋的问题
如果在自旋过程中获得锁,那么之前被阻塞的协程就无法获得。如果加锁的协程特别多,每次都通过自旋获取锁,则之前被阻塞的协程将很难获取锁,从而进入【饥饿状态】。
MutexStarving
Mutex 的模式
每个 Mutex 都有两种模式:Normal, Starving。
Normal 模式
默认情况下的模式就是 Normal。 在该模式下,协程如果加锁不成功,不会立即转入阻塞排队(先进先出),而是判断是否满足自旋条件,如果满足则会启动自旋过程,尝试抢锁。
Starving 模式
Starving
Starving
Woken 状态
Woken 状态用于加锁和解锁过程中的通信。比如,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把 Woken 标记为 1,用于通知解锁协程不必释放信号量,类似知会一下对方,不用释放了,我马上就拿到锁了。
参考资料