hello world

stay foolish, stay hungry

理解 golang 运行时调度

操作系统线程(OS thread)对于 golang 来说太重了。而且最重要的是操作系统(OS)无法基于 golang 模型做出正确的调度。比如,GC的时候要暂停所有的线程(go中对应的是 groutine), 而且内存要处于一致的状态,这需要等待运行中的线程执行到内存一致状态的点(安全点)。当有许多线程时, 为了达到一致状态, 就需要等待这些可能处于任意状态的线程达到一致状态。Golang 的调度器可以做到仅在已知的内存一致状态的点上进行调度。

golang调度器

目前有3种常见的线程模型,一个是N:1模型,即几个用户级线程运行在一个 os 线程,这种模型的优势是可以非常快的进行上下文切换(context switch),缺点是不能充分利用多核 CPU 的优势;另外一个是 1:1 模型,即一个用户级线程对应一个 os 线程,好处是可以利用多核 CPU 的优势,缺点是上下文切换(Content switch)成本比较高。

Golang采用第三种模型 — M:N,即M个用户级线程(goroutine)运行在 N 个 OS 线程上,这样既可以快速的进行上下文切换(context switch),又可以利用多核 CPU 的优势。

如图1所示,Golang 调度器中主要有三种角色:

图1

三角形M代表 OS 线程,可以理解为 machine 的缩写,由系统管理和执行,是 runtime 代码中的 M。

圆形 G 代表 goroutine,有自己的栈、计数器(instruction pointer)和其他调度需要的重要信息,像在等待的 channel 等,是 runtime 代码中的 G。

正方形P代表调度上下文(context),可以理解为 Processor(处理单元),P 表示在单个 os 线程上运行 Go 代码,是实现 N:1 到 M:N 调度的关键,是 runtime 代码中的 P。

图2

如图 2 所示,有两个 os 线程(M),每个 M 有一个 context(P),每个 P 运行一个 goroutine(G)。os 线程(M)要执行 goroutine(G),必须要拿到 context(P)。P 的数量可以在程序启动时, 通过环境变量 GOMAXPROCS 或者 runtime.GOMAXPROCS() 设置,默认值是系统的线程数,而且在程序执行期间通常不会变动,任何时刻只有 GOMAXPROCS 数量的的 P 在执行 go 代码。

灰色的 G 表示没在运行但处于就绪状态的 goroutine,这些 goroutine 在 runqueues 队列中排队等待执行。当代码中调用 go 表达式时,新创建的 goroutine 会添加到 runqueues 队列尾,P 会从 runqueues 队列头取出一个 G,并设置好栈和计数器(instruction pointer),然后开始运行 goroutine。为了降低锁的竞争,除了全局的 runqueues,每个 P 都有自己的 runqueue(早期版本的 Golang 调度器只有一个全局的 runqueue,以至于调度的时候经常因为锁而 block)。

为什么要有 P(context),直接把 runqueue 放到 M 上不是挺好吗?设计 P(context) 的原因是,运行中的 goroutine 如果由于某种原因 block,通过P可以将其移交给其他线程。例如, 当一个系统调用 block 了,也就是 M 被 block 了,这时候需要将 P 从这个 M 上移走,以便 M 可以继续执行其他 P。图 3 中左半部分,M0 执行 G0 时,调用了一个 syscall,而被阻塞了,为了不影响后续 G 的运行,P 将其移交给 M1,M0 继续执行其他 G。当 G0 的 syscall 返回时,M1 尝试获取 P,以便能继续执行 G0,如果获取失败,M1 会将 G0 放到全局的 runqueue 中。

图3

每当P执行完自己的 runqueue 时都会从全局的 runqueue 中获取新的 G,P 也就定时检查全局 runqueue,防止全局 runqueue 上有永远执行不到的 G。如果 Context(P)本地 runqueue 空了,全局的 runqueue 中也没待执行的 G,P 会尝试从别的 P 的 runqueue 队列中窃取(work steal)一半的 groutine,这样就保证了每个 P 都有事做,也就让 M 最大限度的工作。

总结

这篇文章简单描述了 golang 运行时调度,我们了解到:

  • golang 线程模型是 M:N 的
  • M 执行 goroutine 的必要条件是必须获取 processor
  • 如果 goroutine 被阻塞,阻塞的不是 M,而是 processor
  • 每个 P 都有自己的 runqueues,除此之外还有一个全局的 runqueues
  • P 实现了 work steal

参考资料

  1. https://morsmachine.dk/go-scheduler