Go 的调度器 : Ms , Ps 以及 Gs(翻译)

注意,原文发布日期为2017年5月3日,至今已有3年 , 而 go 语言已然从 1.10 进化到了 如今的 1.14 , 所以本篇的内容时效性并不做保证,仅用以学习理解。

基础

​ Go 的 runtime 调度器把 goroutines 映射为操作系统的线程,所以实质上, goroutines 是轻量化版本的,可以以极低的开销运作的线程。 所以 gotoutines 既是 G.M.P 模型里面的 Gruntime 会追踪每一个 G ,并把他们映射到每一个逻辑处理器 (Logical Processors) , 即 PP 可以看作是一种需要被获取到的抽象的资源或者上下文,以使系统的线程 (OS thread) 即 M (Machine) 可以执行 G

​ 你可以通过 runtime.GOMAXPROCS() 来控制讲要使用的 P 的数量 。注意,这条命令尽量只执行一次,因为它会触发 GC ,并导致 STW

​ 事实上,操作系统运行线程,也就是你的代码所运行的地方。 Go 做的 “把戏” 就是,使用编译器把不同系统的系统调用注入 go 的 runtime 内, 所以 Go 可以响应调度器并且执行动作。

无图博客,原图请移步原文

在 Ms , Ps , Gs 间起舞吧(?

Ms , Ps , Gs 之间的交流有点复杂,噢,上帝,看看这美妙的流程图:

无图博客,原图请移步原文

我们可以看到, 这里有两种给 G 用的队列: 一个是实现于 schedt struct ( 极少会被用到的 )的全局队列,以及每一个 P 所维护的一整套 可运行的 G 的队列 。

为了执行 goroutine , M 需要维持一个 context(上下文) P 。然后MP 队列中推出 goroutines ,并执行代码。

当你调度了一个新的 goroutine (执行一个go func()),意味着你把它压入了 P 的队列,他们有一个很有趣的 偷工作 调度算法,当 M 完成执行了一些 G 后 ,它就会设法从队列中取出其他 G , 当其中一个 P 的队列空了之后 , G 会从其他 P 中尝试 “偷” 掉一半的可执行的 G !

当你的 goroutine 做了一个阻塞性的系统调用(syscall)时,有趣的事情就发生了。 阻塞性的系统调用会被拦截, 如果当时有其他的 G 要执行时,runtime 会把这个线程从 P 中分离 ,并创建一个新的 OS 线程 ( 当闲置线程不存在时 ) ,来服务这个处理器。

当系统调用回复时,此 goroutine 会放回本地可执行队列,并且线程会自动释放(*此处用了park ,即停车,应该可以理解为释放为闲置资源。)(意味着线程不再使用) , 并把自己写入闲置线程列表。

如果一个 goroutine 执行了一个网络调用,runtime 会做一相似的动作。这个调用会被拦截,但因为 Go 有一套拥有自己线程的网络池 , 它们会被分配到这些任务。

本质上,当目前的gooutine 被阻塞在:

  • 阻塞的系统调用(比如打开一个文件),
  • 网络输入,
  • go channel 操作,
  • 原生的 sync包调用。

时,Go 的 runtime 会执行另外的 goroutine