golang的GMP调度

Golang 调度器四个重要结构 :M P G Sched

GMP的结构源码在文件中\src\runtime\runtime2.go

简介

  • G :goroutine,go程序建立的用户线程。主要保存 goroutine 的运行时栈信息(stack结构体)以及 CPU 的一些寄存器的值(gobuf结构体),还有关联的M,全局队列中下个G等信息。
  • M :machine 一个 M 直接关联一个os内核线程,用于执行G。 M 会优先从关联的 P 的本地队列中直接获取待执行的 G ,它保存了 M 自身使用的栈信息、当 前正在 M 上执行的 G 信息、与之绑定的 P 信息。
  • P :processor 代表了 M 所需的上下文环境,也是处理用户级代码逻辑的处理器,可以看作一个局部调度器使go代码在一个线程上跑。
  • P列表 :在创建程序的时候创建一个 P 列表, 最多有$GOMAXPROCS个,这环境变量可以通过操作系统中的环境变量设置,也可以通过Go程序中的runtime.GOMAXPROCS()函数设置,默认为处理器的核心数,它代表了真正的并发度。
  • M列表 :当前操作系统分配到当前go程序的内核线程数,可以通过go语言中runtime/debug包中的SetMaxThreads函数设置。当有一个 M 阻塞,会有一个新的M被创建;当有一个 M 空闲,会被回收或睡眠。
  • P的本地队列 :P维护一个runq_用来存放等待执行的goroutine,新创建的 G 会优先放在 P 的本地队列,当本地队列满(256G)时,会放入 G 的全局队列。
  • 全局队列 :如果 P 的本地队列已满,待执行的 G 就会放在全局队列中, M 会先从关联的 P本地队列中获取 待执行的 G ,没有的话,再到 全局队列 中获取;如果这里也没有了,就去 其他 P 的本地队列 中获取一些任务。

GMP调度

image.png

上图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文(context)P,一个上下文连接一个或者多个Goroutine。图中P正在执行的 Goroutine 为蓝色的;处于待执行状态的 Goroutine 为灰色的,灰色的 Goroutine 形成了一个队列 runqueues 。P(Processor)的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数 runtime.GOMAXPROCS() 进行设置。Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是我们要执行并发的代码。

为何要维护多个上下文P?因为当一个OS线程被阻塞时,P可以转而投奔另一个OS线程!

下图中看到,当一个OS线程M0陷入阻塞时,P转而在OS线程M1上运行。调度器保证有足够的线程来运行所以的context P。一个很简单的例子就是系统调用 syscall ,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的 Goroutine 被调度执行。

image.png

如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能本身就存在,没创建),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行 Goroutine 队列中的其他 Goroutine

当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,然后自己放到线程池或者转入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。

image.png

另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了一个上下文P闲着没事儿干而系统却任然忙碌。但是如果global runqueue没有任务G了,那么P就不得不从其他的上下文P那里拿一些G来执行。一般来说,如果上下文P从其他的上下文P那里要偷一个任务的话,一般就‘偷’run queue的一半,这就确保了每个OS线程都能充分的使用。

有疑问加站长微信联系(非本文作者)