go语言调度gmp原理(3)

发布时间 2023-05-16 21:20:54作者: 每天提醒自己要学习

go语言调度gmp原理(3)

调度循环

调度器启动之后,go语言运行时会调用runtime.mstart和runtime.mstart1,前者会初始化g0的stackguard0和stackguard1字段,后者会初始化线程并调用runtime.schedule进入调度循环

func schedule() {
	mp := getg().m

	if mp.locks != 0 {
		throw("schedule: holding locks")
	}

	if mp.lockedg != 0 {
		stoplockedm()
		execute(mp.lockedg.ptr(), false) // Never returns.
	}

	// We should not schedule away from a g that is executing a cgo call,
	// since the cgo call is using the m's g0 stack.
	if mp.incgo {
		throw("schedule: in cgo")
	}

top:
	pp := mp.p.ptr()
	pp.preempt = false

	// Safety check: if we are spinning, the run queue should be empty.
	// Check this before calling checkTimers, as that might call
	// goready to put a ready goroutine on the local run queue.
	if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}

	gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

	// This thread is going to run a goroutine and is not spinning anymore,
	// so if it was marked as spinning we need to reset it now and potentially
	// start a new spinning M.
	if mp.spinning {
		resetspinning()
	}

	if sched.disable.user && !schedEnabled(gp) {
		// Scheduling of this goroutine is disabled. Put it on
		// the list of pending runnable goroutines for when we
		// re-enable user scheduling and look again.
		lock(&sched.lock)
		if schedEnabled(gp) {
			// Something re-enabled scheduling while we
			// were acquiring the lock.
			unlock(&sched.lock)
		} else {
			sched.disable.runnable.pushBack(gp)
			sched.disable.n++
			unlock(&sched.lock)
			goto top
		}
	}

	// If about to schedule a not-normal goroutine (a GCworker or tracereader),
	// wake a P if there is one.
	if tryWakeP {
		wakep()
	}
	if gp.lockedm != 0 {
		// Hands off own p to the locked m,
		// then blocks waiting for a new p.
		startlockedm(gp)
		goto top
	}

	execute(gp, inheritTime)
}

runtime.schedule函数会从下面几处查找待执行的goroutine

  1. 为了保证公平,当全局运行队列中有待执行的goroutine时,通过schedtick保证有一定概率会从全局的运行队列中查找对应的goroutine
  2. 从处理器本地的运行队列中查找待执行的goroutine
  3. 如果前两种方法都没有找到goroutine,会通过runtime.findrunnable阻塞地查找goroutine

runtime.findrunnable获取goroutine过程

  1. 从本地运行队列、全局运行队列中查找
  2. 从网络轮询器中查找是否有goroutine等待运行
  3. 通过runtime.runqsteal尝试从其他随机的处理器中窃取待运行的goroutine,该函数还可能窃取处理器的计时器

当前函数一定会返回一个可执行的goroutine,如果当前不存在就会阻塞地等待

接下来由runtime.execute执行获取的goroutine,做好准备后,它会通过runtime.gogo将goroutine调度到当前线程上

func execute(gp *g, inheritTime bool) {
	mp := getg().m

	if goroutineProfile.active {
		// Make sure that gp has had its stack written out to the goroutine
		// profile, exactly as it was when the goroutine profiler first stopped
		// the world.
		tryRecordGoroutineProfile(gp, osyield)
	}

	// Assign gp.m before entering _Grunning so running Gs have an
	// M.
	mp.curg = gp
	gp.m = mp
	casgstatus(gp, _Grunnable, _Grunning)
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + _StackGuard
	if !inheritTime {
		mp.p.ptr().schedtick++
	}

	// Check whether the profiler needs to be turned on or off.
	hz := sched.profilehz
	if mp.profilehz != hz {
		setThreadCPUProfiler(hz)
	}

	if trace.enabled {
		// GoSysExit has to happen when we have a P, but before GoStart.
		// So we emit it here.
		if gp.syscallsp != 0 && gp.sysblocktraced {
			traceGoSysExit(gp.sysexitticks)
		}
		traceGoStart()
	}

	gogo(&gp.sched)
}

runtime.gogo从runtime.gobuf中取出了runtime.goexit的程序计数器和待执行函数的程序计数器,其中:

  • runtime.goexit的程序计数器被放到栈的SP上
  • 待执行函数的程序计数器被放到了寄存器BX上

正常的函数调用都会使用CALL指令,该指令会将调用方的返回地址加入栈寄存器SP中,然后跳转到目标函数;当目标函数返回后,会从堆中查找调用的地址并跳转回调用方继续执行剩余代码

当goroutine中运行的函数返回时,就会跳转到runtime.goexit所在位置执行该函数

经过一系列复杂的函数调用,我们最终在当前线程的g0的栈上调用runtime.goexit0函数,该函数会将goroutine转换为_Gdead状态、清除其中的字段、移除goroutine和线程的关联,并调用runtime.gfput重新加入处理器的goroutine空闲列表gFree

最后runtime.goexit0会重新调用runtime.schedule触发新一轮的goroutine调度,go语言中的运行时,调度循环会从runtime.schedule开始,最终又回到runtime.schedule

这里介绍的是goroutine正常执行并退出的逻辑,实际情况会复杂得多,多数情况下goroutine在执行过程中会经历写作时调度或者抢占式调度