源码版本:const TheVersion = `go1.16.4`
备注:
源码起源:
runtime.main()
调度
runtime.schedule()
调度 启动/暂停 文档说明 (内容来自于 runtime/proc.go 文件中上面的注释)
我们需要在保持足够的运行工作线程以利用可用的硬件并行性和停放过多运行的工作线程以节省 CPU 资源和功率之间取得平衡。这并不简单,原因有二:
- 调度器状态是有意分布的(特别是每 P 个工作队列),因此不可能在快速路径上计算全局谓词;
- 为了优化线程管理,我们需要知道未来(在不久的将来准备好新的 goroutine 时不要停放工作线程)。
三种被拒绝但效果不佳的方法:
- 集中所有调度器状态(会抑制可扩展性)。
- 直接 goroutine 切换。也就是说,当我们准备好一个新的 goroutine 并且有一个空闲的 P 时,取消驻留线程并将线程和 goroutine 移交给它。这将导致线程状态抖动,因为准备好 goroutine 的线程可能会在下一次停止工作片刻,我们需要停车。此外,它会破坏计算的局部性,因为我们想在同一个线程上保留依赖的 goroutine;并引入额外的延迟。
- 当我们准备好一个 goroutine 并且有一个空闲的 P 时,取消一个额外的线程,但不要进行切换。这将导致过多的线程停放/取消停放,因为额外的线程将立即停放而没有发现任何工作要做。
目前的做法:
当我们准备好一个 goroutine 时,如果 (1) 有一个空闲的 P 并且没有“自旋”的工作线程,我们就会解除一个额外的线程。如果工作线程不在本地工作并且在全局运行队列/netpoller 中没有找到工作,则认为工作线程正在自旋;自旋状态在 m.spinning 和 sched.nmspinning 中表示。以这种方式释放的线程也被认为是自旋的;我们不进行 goroutine 切换,因此这些线程最初无法工作。自旋线程在停车前会在 per-P 运行队列中进行一些自旋寻找工作。如果一个自旋线程找到工作,它会将自己从自旋状态中取出并继续执行。如果它没有找到工作,它会自行退出自旋状态,然后停止。如果至少有一个自旋线程(sched.nmspinning>1),我们在准备 goroutine 时不会取消新线程。为了弥补这一点,如果最后一个自旋线程找到工作并停止自旋,它必须解开一个新的自旋线程。这种方法消除了不合理的线程释放峰值,但同时保证最终的最大 CPU 并行度利用率。主要的实现复杂性是我们需要在自旋->非自旋线程转换过程中非常小心。这种转换可能会随着新 goroutine 的提交而竞争,并且其中一个部分或另一个部分需要解除另一个工作线程的驻留。如果他们都未能做到这一点,我们最终可能会出现半持久的 CPU 未充分利用。 goroutine 准备的一般模式是:提交一个 goroutine 到本地工作队列,#StoreLoad 样式的内存屏障,检查 sched.nmspinning。 spin->non-spinning 过渡的一般模式是:递减 nmspinning,#StoreLoad 风格的内存屏障,检查所有 per-P 工作队列是否有新工作。请注意,所有这些复杂性都不适用于全局运行队列,因为我们在提交到全局队列时不会马虎地取消线程。另请参阅 nmspinning 操作的注释。
