todo:

  • 如何实现抢占式调度
  • 如何防止 goroutine 长时间占用 cpu
  • 完善调度逻辑
  • 如何实现工作窃取

源码位置: src/runtime.proc.go

调度初始化

启动流程:

  1. 调用 osinit ,获取 cpu 数量、大页内存大小(全局变量)
  2. 调用 schedinit ,初始化调度系统(竞态检测器 -race 标志,各种锁、内存分配器、垃圾回收器等等)
    schedinit 中还会检测 GOMAXPROCS 这个参数,默认使用 cpu 的数量作为 GOMAXPROCS 来创建并启动所有P(Processor),此时M0 已经和其中一个 P 进行了绑定
var procs int32
if n, ok := strconv.Atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
	procs = n
	sched.customGOMAXPROCS = true
} else {
	// Use numCPUStartup for initial GOMAXPROCS for two reasons:
	//
	// 1. We just computed it in osinit, recomputing is (minorly) wasteful.
	//
	// 2. More importantly, if debug.containermaxprocs == 0 &&
	//    debug.updatemaxprocs == 0, we want to guarantee that
	//    runtime.GOMAXPROCS(0) always equals runtime.NumCPU (which is
	//    just numCPUStartup).
	procs = defaultGOMAXPROCS(numCPUStartup)
}

// 创建 P:processor
if procresize(procs) != nil {
	throw("unknown runnable goroutine during bootstrap")
}


....
// start the world
worldStarted()

开始调度

func schedule() {
	mp := getg().m // 获取当前g绑定的m

	// mp.locks 是一个计数器, 记录当前 M 持有的锁的数量
	// 如果 M 持有锁还参与调度新的goroutine, 可能会导致死锁
	if mp.locks != 0 {
		throw("schedule: holding locks")
	}
	// lockedg指向了被绑定到当前 M 的 goroutine
	// 有一个类似的是 g.lockedm: 指向当前 goroutine 绑定的 M
	// m.lockedg 和 g.lockedm 形成双向绑定,确保 goroutine 只能在 特定的 M 上执行
	if mp.lockedg != 0 {
		// g与M绑定了,但g可能不在当前M上运行,需要等待g重新调度到当前M上
		// 由于 M和g绑定了,所以M不能执行其他g
		stoplockedm() // 停止当前 M, 这里会阻塞住,只要g重新调度到当前M上
		// 执行锁定的g
		execute(mp.lockedg.ptr(), false) // Never returns.
	}

	// 处理cgo 调用
	// 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:
	// 获取当前p
	pp := mp.p.ptr()
	// 清除抢占标志
	pp.preempt = false

	// 安全检查: 如果我们在 spinning, 那么 run queue 应该为空。
	// 在调用 checkTimers 之前检查这个,因为那个可能会调用 goready 把一个就绪的 goroutine 放到本地 run queue 中。
	if mp.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
		throw("schedule: spinning with local work")
	}
	// 找一个可执行的 goroutine
	gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available

	// findRunnable may have collected an allp snapshot. The snapshot is
	// only required within findRunnable. Clear it to all GC to collect the
	// slice.
	// 清除快照
	// 清除 allp snapshot, 因为 findRunnable 可能会收集一个 allp snapshot
	// 这个 snapshot 只在 findRunnable 内部需要, 所以需要清除, 让 GC 可以收集这个 slice。
	mp.clearAllpSnapshot()

	if debug.dontfreezetheworld > 0 && freezing.Load() {
		// See comment in freezetheworld. We don't want to perturb
		// scheduler state, so we didn't gcstopm in findRunnable, but
		// also don't want to allow new goroutines to run.
		//
		// Deadlock here rather than in the findRunnable loop so if
		// findRunnable is stuck in a loop we don't perturb that
		// either.
		lock(&deadlock)
		lock(&deadlock)
	}
	// 这个线程将要运行一个 goroutine 并且不再 spinning 了,
	// 所以如果它被标记为 spinning 我们需要现在重置它并且可能启动一个新的 spinning M。
	// 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()
	}

	// 处理调度禁用, 将goroutine放进 待运行队列
	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 {
			// 将当前不允许调度的 goroutine(gp)加入到待运行队列 sched.disable.runnable,
			// 等后续恢复 user 调度后会重新考虑这些 goroutine 的调度。
			sched.disable.runnable.pushBack(gp)
			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()
	}
	// lockedm 不为0 代表这个goroutine 被某个线程锁定了, 需要把当前的p 让渡给被锁定的m
	if gp.lockedm != 0 {
		// Hands off own p to the locked m,
		// then blocks waiting for a new p.
		// locked m 代表被某个 goroutine(通常通过 runtime.LockOSThread 或 Cgo 绑定)锁定的操作系统线程(M)。
		// 之所以会 "locked",是因为有些 goroutine 需要专门绑定和固定在一个特定的操作系统线程上执行,
		// 这样的需求出现在 cgo 调用、系统回调、线程本地存储等对线程有强依赖的场景。
		// 因此调度器要把当前的 P 让渡给被锁定的 m,否则不会满足用户 goroutine 的 LockOSThread 语义要求。
		// 场景: g1被锁定在m执行,但g1是在m2上被调度,所以需要把 p让渡给m1,让m1去执行g1
		startlockedm(gp)
		goto top
	}

	execute(gp, inheritTime)
}

getg () 以及当前 goroutine 的理解

getg 的作用:返回【当前 goroutine】的指针

“当前 goroutine” 是指在 Go 语言程序执行过程中,正在运行的 goroutine。每当程序运行到某个 goroutine 的执行代码时,这个 goroutine 就被称为“当前 goroutine”。

如果在 M1 线程调用 getg() ,返回的是 M1 正在执行的 goroutine 的指针

值得注意的是: getg() 这是一个编译器内联的函数, 没有函数调用开销,编译器会将 getg () 的调用重写为一个"直接获取 g 的指令",它会通过 TLS (线程本地存储-Thread Local Storage) 或者寄存器直接获取

todo:如何防止 goroutine 长时间占用 CPU

关键词:sysmon 抢占式调度

todo:M 自旋的目的和触发条件

目的:

  1. M 没有工作的时候,通过自旋寻找其他 P 上的工作 (steal work from other Processor)
  2. 避免线程切换
  3. 快速响应新提交的工作

触发自旋的条件:

  1. 有一半的活跃 P 时就可以自旋
  2. 自旋 M 数量不足

进入自旋后,就开始从其他 P 窃取工作了