mutex有两个成员,state和sema。其中 state
表示当前互斥锁的状态,而 sema
是用于控制锁状态的信号量。有两个要实现的接口,加锁和解锁。
type Mutex struct {
state int32
sema uint32
}
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
state是一个32位的值,第一位是加锁标记,第二位是唤醒标记,第三位是饥饿标记,之后的位数用来记录等待队列的数量。
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
mutexLocked
— 表示互斥锁的锁定状态;mutexWoken
— 表示从正常模式被从唤醒;mutexStarving
— 当前的互斥锁进入饥饿状态;waitersCount
— 当前互斥锁上等待的 Goroutine 个数;
互斥锁有两种操作模式:普通模式和饥饿模式。 在普通模式下,等待者按照先进先出(FIFO)的顺序排队,但是被唤醒的等待者不拥有互斥锁,而是与新到达的 goroutine 竞争互斥锁的所有权。新到达的 goroutine 有优势——它们已经在 CPU 上运行,并且可能有很多,因此被唤醒的等待者很可能会失败。在这种情况下,它会被排在等待队列的最前面。如果等待者在超过 1 毫秒无法获得互斥锁,它将把互斥锁切换到饥饿模式。
在饥饿模式下,互斥锁的所有权直接从解锁的 goroutine 转移到等待队列的最前面的等待者。新到达的 goroutine 不会尝试获取互斥锁,即使它看起来是解锁的,也不会尝试自旋。相反,它们将自己排队在等待队列的尾部。
如果一个等待者获得了互斥锁的所有权,
(1)它是队列中的最后一个等待者,
(2)它等待时间不到 1 毫秒,
它将把互斥锁切换回普通操作模式。
普通模式具有更好的性能,因为一个 goroutine 即使有阻塞的等待者,也可以连续多次获得互斥锁。 饥饿模式对于防止尾部延迟的情况非常重要。
锁定
func (m *Mutex) Lock() {
// 快速路径
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 慢速路径
m.lockSlow()
}
锁定操作,首先会判断是不是可以直接拿到锁,也就是没有竞争的情况下会直接返回,如果不可以的话使用慢速路径,下边是慢速路径的具体实现。
func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
// 这里做了两个判断,1、饥饿模式的判断,2、是否可以自旋
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 满足自选条件,尝试尝试设置 mutexWoken 标志以通知 Unlock,不唤醒其他阻塞的goroutine。
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// 当前锁处于非饥饿模式且没有加锁,标记加锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
//已加锁或者饥饿模式的状态,不能继续自选,等待队列+1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 饥饿状态,且锁定,标记饥饿
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
if awoke {
// goroutine 已从睡眠中唤醒,
// 因此我们需要在任何情况下重置标志。
if new&mutexWoken == 0 {
throw("sync: 互斥锁状态不一致")
}
//重置唤醒状态
new &^= mutexWoken
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
break // 使用 CAS 锁定互斥锁
}
// 如果之前已经在等待了,将排队在队列的前面。
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 {
// 如果此 goroutine 被唤醒并且互斥锁处于饥饿模式,
// 所有权已经交给我们,但互斥锁的状态有些不一致:
// mutexLocked 未设置,但我们仍然被认为是等待者。修复这个问题。
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: 互斥锁状态不一致")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
// 退出饥饿模式。
// 在这里执行是至关重要的,并考虑等待时间。
// 饥饿模式是如此低效,以至于两个 goroutine
// 一旦它们将互斥锁切换到饥饿模式,就可以无限地进行锁步。
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
runtime_canSpin 此时是否适合进行自旋。
runtime_doSpin 自旋
首先会判断是否自旋
是否自选需要满足以下条件
- 运行在多 CPU 的机器上;
- 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空;
计算锁的最新锁的状态
尝试获取锁
释放
释放代码,主要是将锁位清除,也是一个快速,清除锁状态,一个慢速,处理其他位置
// Unlock 解锁 m。
// 如果在调用 Unlock 时 m 没有被锁定,则会引发运行时错误。
//
// 已锁定的 Mutex 不与特定的 goroutine 关联。
// 允许一个 goroutine 锁定 Mutex,然后安排另一个 goroutine 解锁它。
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 快速路径:将锁位清除。
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
//如果不为0表示在饥饿状态或者等待队列不为空
m.unlockSlow(new)
}
}
下边是慢速解锁的操作
func (m *Mutex) unlockSlow(new int32) {
if (new+mutexLocked)&mutexLocked == 0 {
fatal("sync: unlock of unlocked mutex")
}
if new&mutexStarving == 0 {
old := new
for {
// 如果没有等待者,或者一个 goroutine 已经被唤醒或抢占了锁,则无需唤醒任何人。
// 在饥饿模式下,锁的所有权直接从解锁 goroutine 移交给下一个等待者。我们不是这个链条的一部分,
// 因为我们在上面解锁互斥锁时没有观察到 mutexStarving。所以让开道。
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 {
// 饥饿模式:将互斥锁所有权移交给下一个等待者,并放弃我们的时间片,以便下一个等待者可以立即开始运行。
// 注意:如果未设置 mutexLocked,则不会设置 mutexLocked,等待者会在唤醒后设置它。
// 但是如果设置了 mutexStarving,则仍然认为锁被锁定,因此新到达的 goroutine 不会获取它。
runtime_Semrelease(&m.sema, true, 1)
}
}
首先会检查锁状态的合法性 ,如果当前互斥锁已经被解锁过了会直接抛出异常 “sync: unlock of unlocked mutex” 中止当前程序
之后针对,正常模式和饥饿模式分别做了处理
正常模式
- 如果互斥锁不存在等待者或者互斥锁的 mutexLocked、mutexStarving、mutexWoken 状态不都为 0,也就是有另一个goroutine已经被唤醒或者获取了锁,那么当前方法可以直接返回,不需要唤醒其他等待者;
- 如果互斥锁存在等待者,会通过 sync.runtime_Semrelease 唤醒等待者并移交锁的所有权;
饥饿模式
- 将当前锁交给下一个正在尝试获取锁的等待者,等待者被唤醒后会得到锁,在这时互斥锁还不会退出饥饿状态
在加锁和解锁中用到了两个方法
runtime_SemacquireMutex(&m.sema, queueLifo, 1) runtime_Semrelease(&m.sema, false, 1)
// 在 runtime 包中定义
// Semacquire 在 *s > 0 时等待,然后对其进行原子递减。
// 它旨在作为同步库使用的简单睡眠原语,并不应直接使用。
func runtime_Semacquire(s *uint32)
// Semacquire(RW)Mutex(R) 类似于 Semacquire,但用于对竞争的 Mutex 和 RWMutex 进行分析。
// 如果 lifo 为 true,则将等待者排队到等待队列的头部。
// skipframes 是在跟踪时要忽略的帧数,从 runtime_SemacquireMutex 的调用者开始计数。
// 这个函数的不同形式告诉运行时如何在回溯中呈现等待的原因,并用于计算一些指标。
// 否则,它们在功能上是相同的。
func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int)
// Semrelease 原子地递增 *s,并在 Semacquire 中有一个被阻塞的 goroutine 时通知它。
// 它旨在作为同步库使用的简单唤醒原语,并不应直接使用。
// 如果 handoff 为 true,则将计数直接传递给第一个等待者。
// skipframes 是在跟踪时要忽略的帧数,从 runtime_Semrelease 的调用者开始计数。
func runtime_Semrelease(s *uint32, handoff bool, skipframes int)
实现方法
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) {
gp := getg()
if gp != gp.m.curg {
throw("semacquire 不在 G 栈上")
}
// 简单情况。
if cansemacquire(addr) {
return
}
// 更难的情况:
// 增加等待者计数
// 再次尝试 cansemacquire,如果成功则返回
// 将自己排队为等待者
// 睡眠
// (等待者描述符由信号器出队)
s := acquireSudog()
root := semroot(addr)
t0 := int64(0)
s.releasetime = 0
s.acquiretime = 0
s.ticket = 0
if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
if t0 == 0 {
t0 = cputicks()
}
s.acquiretime = t0
}
for {
lockWithRank(&root.lock, lockRankRoot)
// 将自己添加到 nwait 以禁用 "简单情况" 中的 semrelease。
atomic.Xadd(&root.nwait, 1)
// 检查 cansemacquire 以避免错过唤醒。
if cansemacquire(addr) {
atomic.Xadd(&root.nwait, -1)
unlock(&root.lock)
break
}
// 任何 cansemacquire 后的 semrelease 都知道我们在等待
// (我们在上面设置了 nwait),所以进入睡眠状态。
root.queue(addr, s, lifo)
goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
if s.ticket != 0 || cansemacquire(addr) {
break
}
}
if s.releasetime > 0 {
blockevent(s.releasetime-t0, 3+skipframes)
}
releaseSudog(s)
}
func semrelease1(addr *uint32, handoff bool, skipframes int) {
root := semroot(addr)
atomic.Xadd(addr, 1)
// 简单情况:没有等待者?
// 这个检查必须在 xadd 之后进行,以避免错过唤醒
// (参见 semacquire 中的循环)。
if atomic.Load(&root.nwait) == 0 {
return
}
// 较难的情况:搜索等待者并唤醒它。
lockWithRank(&root.lock, lockRankRoot)
if atomic.Load(&root.nwait) == 0 {
// 计数已被另一个 goroutine 消耗,
// 因此不需要唤醒另一个 goroutine。
unlock(&root.lock)
return
}
s, t0 := root.dequeue(addr)
if s != nil {
atomic.Xadd(&root.nwait, -1)
}
unlock(&root.lock)
if s != nil { // 可能是缓慢的或甚至让步,所以先解锁
acquiretime := s.acquiretime
if acquiretime != 0 {
mutexevent(t0-acquiretime, 3+skipframes)
}
if s.ticket != 0 {
throw("损坏的信号量票")
}
if handoff && cansemacquire(addr) {
s.ticket = 1
}
readyWithTime(s, 5+skipframes)
if s.ticket == 1 && getg().m.locks == 0 {
// 直接 G 移交
// readyWithTime 已经将等待的 G 添加为当前 P 中的 runnext;
// 现在我们调用调度程序,以便立即开始运行等待的 G。
// 请注意,等待者继承我们的时间片:这是可取的,
// 以避免高度争用的信号量无限期地占用 P。
// goyield 类似于 Gosched,但它会发出“被抢占”跟踪事件,
// 更重要的是,它将当前 G 放在本地 runq 而不是全局 runq 中。
// 我们只在饥饿状态下执行此操作(handoff=true),
// 因为在非饥饿情况下,另一个等待者可能会在我们让出/调度期间
// 获取信号量,这是浪费的。我们等待进入饥饿状态,
// 然后开始执行票证和 P 的直接移交。
// 有关讨论,请参见问题 33747。
goyield()
}
}
}
首先 rutime 会根据 m.sema 的地址通过哈希计算来生成一个 table,每个 mutex 的信号量地址对应一个表。每个表都有一个 semaRoot 的对象,这个对象包含一个 treap *sudog
链表结构队列。通过 semaroot.g = getg()
把当前的 g 绑定起来就进入到等待着队列了。