go mutex实现分析

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 自旋

首先会判断是否自旋

        是否自选需要满足以下条件

  1. 运行在多 CPU 的机器上;
  2. 当前 Goroutine 为了获取该锁进入自旋的次数小于四次;
  3. 当前机器上至少存在一个正在运行的处理器 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” 中止当前程序

之后针对,正常模式和饥饿模式分别做了处理

正常模式

  1. 如果互斥锁不存在等待者或者互斥锁的 mutexLocked、mutexStarving、mutexWoken 状态不都为 0,也就是有另一个goroutine已经被唤醒或者获取了锁,那么当前方法可以直接返回,不需要唤醒其他等待者;
  2. 如果互斥锁存在等待者,会通过 sync.runtime_Semrelease 唤醒等待者并移交锁的所有权;

饥饿模式

  1. 将当前锁交给下一个正在尝试获取锁的等待者,等待者被唤醒后会得到锁,在这时互斥锁还不会退出饥饿状态

在加锁和解锁中用到了两个方法

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 绑定起来就进入到等待着队列了。

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值