Go的GMP协程调度器

Posted by 大攀 on Sunday, November 29, 2020

TOC

GMP调度模型

GMP调度模型

设计策略

  1. 复用线程

  避免频繁创建、销毁线程,而是对线程的复用。

  • work stealing 机制

  当本地线程无可运行的G时,尝试从其它线程绑定的P中偷取G(如果Global Queue有待运行的G,就先从Global队列中去取),而不是销毁线程。

  • hand off 机制

  当本线程M因为G进行系统调用阻塞时,线程就先释放绑定的P,把P转移到其它空闲的线程M执行。

  1. 利用并行

  GOMAXPROCS 设置P的数量,最多有GOMAXPROCS 个线程分布在多个CPU上同时运行(runtime/debug 中的SetMaxThreads 设置M最大数量)。GOMAXPROCS 也限制了并发的程度, 比如GOMAXPROCS = 物理核数/2,最多利用一半的CPU核进行并行。

  1. 抢占式任务处理

  在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中一个goroutine最多占用CPU 10ms,防止其它goroutine被饿死。

  1. 全局G队列

  当一个G被创建时,先尝试加入P的本地G队列,如果局部队列已满则加入全局G队列。

go func()调度流程

  1. go func()创建一个goroutine;
  2. 新创建的G先尝试加入P的本地G队列,如果局部队列已满则加入全局G队列;
  3. G只能在M中运行,一个M必须持有一个P。M从与它绑定的P中获取可执行状态的G,如果P本地队列为空,则从全局G队列或其它MP组合中偷取可执行的G;
  4. M调度G,执行G.func(),顺利执行完后销毁G,返回M响应;
  5. 当M执行G时发生了系统调用(syscall)或其余阻塞操作,M会阻塞,runtime会把这个M绑定的P摘除(detach),然后再创建一个新M(如果有空闲的M,优先复用空闲的M)与被摘除的这个P绑定;
  6. 当M的系统调用结束后,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中。

调度器的生命周期

调度器的生命周期

M0

  M0是启动程序后编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要再heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其它M一样了。

G0

  G0每次启动一个M都会第一个创建的goroutine,每个M都会有一个自己的G0,G0仅用于负责调度其它G,G0不指向任何可执行的函数。在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0.

调度器调度典型场景

  1. 有子协程的创建:局部性原理,P拥有G1,M1获取P后开始运行G1,G1使用go func()创建了G2,为了局部性G2优先加入到P1的本地队列。
  2. G0协程切换:G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用。
  3. 本地G队列已满:负载均衡把已满的本地队列中前一半的G,还有新创建的G一起转移到全局G队列(顺序会被打乱)。
  4. 自旋线程:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合。被唤醒的M绑定了P,并运行了G0,但其本地队列没有G,此时这个被唤醒的M为自旋线程(没有G但为运行状态的线程,不断寻找G)。
  5. 从全局队列到P本地队列的负载均衡:M1尝试从全局队列取一批G放到P2的本地队列(函数:findrunnable())。M1从全局队列取的G数量n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))。
  6. work stealing:在场景5的情况下,全局队列如果没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。
  7. 所有P的本地队列都为空:最多有GOMAXPROCS个自旋的线程等待处理G,当有新goroutine创建时,立刻能有M运行它,如果销毁再新建就增加了时延,降低了效率。
  8. 因为永远是M>=P,大部分都是M在抢占需要运行的P: G1运行时创建了G2,G1进行了阻塞的系统调用,M1和P1立即解绑,P1会执行以下判断:如果P1本地队列有G、全局队列有G或有空闲的M,P1都会立马唤醒1个M和它绑定,否则P1则会加入到空闲P列表,等待M来获取可用的p。
  9. G1创建了G2,G1进行了非阻塞系统调用: M1和P1会解绑,但M1会记住P1,然后G1和M1进入系统调用状态。当G1和M1退出系统调用时,会尝试获取P1,如果无法获取,则获取空闲的P,如果依然没有,G1会被记为可运行状态,并加入到全局队列,M1因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。

comments powered by Disqus