go语言调度gmp原理(4)

zpf253 / 2023-05-18 / 原文

go语言调度gmp原理(4)

触发调度

下面简单介绍所有触发调度的时间点。因为调度器的runtime.schedule会重新选择goroutine在线程上执行,所以我们只要找到该函数的调用方,就能找到所有触发调度的时间点

这里重点介绍运行时触发调度的几条路径

  • 主动挂起——runtime.gopark -> runtime.park_m
  • 系统调用——runtime.exitsyscall -> runtime.exitsyscall0
  • 协作式调度——runtime.Gosched -> runtime.gosched_m -> runtime.goschedIpml
  • 系统监控——runtime.sysmon -> runtime.retake -> runtime.preemptone

这里介绍的调度时间点不是将线程的运行权直接交给其他任务,而是通过调度器的runtime.schedule重新调度。

主动挂起

runtime.gopark是触发调度最常用的方法,该函数会将当前goroutine暂停,暂停的任务不会放回运行队列。下面分析该函数的实现原理

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
	if reason != waitReasonSleep {
		checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
	}
	mp := acquirem()
	gp := mp.curg
	status := readgstatus(gp)
	if status != _Grunning && status != _Gscanrunning {
		throw("gopark: bad g status")
	}
	mp.waitlock = lock
	mp.waitunlockf = unlockf
	gp.waitreason = reason
	mp.waittraceev = traceEv
	mp.waittraceskip = traceskip
	releasem(mp)
	// can't do anything that might move the G between Ms here.
	mcall(park_m)
}

上述函数会通过runtime.mcall切换到g0的栈上调用runtime.park_m

func park_m(gp *g) {
	mp := getg().m

	if trace.enabled {
		traceGoPark(mp.waittraceev, mp.waittraceskip)
	}

	// N.B. Not using casGToWaiting here because the waitreason is
	// set by park_m's caller.
	casgstatus(gp, _Grunning, _Gwaiting)
	dropg()

	if fn := mp.waitunlockf; fn != nil {
		ok := fn(gp, mp.waitlock)
		mp.waitunlockf = nil
		mp.waitlock = nil
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
	schedule()
}

runtime.park_m会将当前goroutine的状态从_Grunning切换至_Gwaiting,调用runtime.dropg移除线程和goroutine之间的关联,此后就可以调用runtime.schedule触发新一轮调度了

当goroutine等待的特定条件满足后,运行时会调用runtime.goready将因调用runtime.gopark而陷入休眠的goroutine唤醒

func goready(gp *g, traceskip int) {
	systemstack(func() {
		ready(gp, traceskip, true)
	})
}

func ready(gp *g, traceskip int, next bool) {
	if trace.enabled {
		traceGoUnpark(gp, traceskip)
	}

	status := readgstatus(gp)

	// Mark runnable.
	mp := acquirem() // disable preemption because it can be holding p in a local var
	if status&^_Gscan != _Gwaiting {
		dumpgstatus(gp)
		throw("bad g->status in ready")
	}

	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
	casgstatus(gp, _Gwaiting, _Grunnable)
	runqput(mp.p.ptr(), gp, next)
	wakep() //根据条件唤醒新的p
	releasem(mp) //解除禁止抢占
}

runtime.ready会将准备就绪的goroutine的状态切换至_Grunnable,并将其加入处理器的运行队列中,等待调度器的调度