todo:
- 如何实现抢占式调度
- 如何防止 goroutine 长时间占用 cpu
- 完善调度逻辑
- 如何实现工作窃取
源码位置: src/runtime.proc.go
调度初始化
启动流程:
- 调用
osinit,获取 cpu 数量、大页内存大小(全局变量) - 调用
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 自旋的目的和触发条件
目的:
- M 没有工作的时候,通过自旋寻找其他 P 上的工作 (steal work from other Processor)
- 避免线程切换
- 快速响应新提交的工作
触发自旋的条件:
- 有一半的活跃 P 时就可以自旋
- 自旋 M 数量不足
进入自旋后,就开始从其他 P 窃取工作了