调度模型


在1.1版本后,Go采用GMP模型调度协程,其逻辑结构如下图所示。

image-20220505211827389

调度模型由4类对象schedt,m,p,g组成,其中:

schedt对象:scheduler,整个进程一个实例,代表整个调度框架。

m对象:machine,每个m对象绑定一个操作系统线程,调度goroutine。

p对象:processor,其个数一般和进程所在容器(物理机,虚拟机,容器等)的核数相同,负责维护g对象和本地内存管理。

g对象:goroutine,程序最小执行单元,进程中有大量的goroutine,其数量一般来说没有上限。

正如上节讲到的通用的携程调度模型,在GMP模型中,goroutine被m周而复始的调度,但是在调度过程中运用了比较复杂的算法,以求达到最佳的调度效率。下面将详细讲解上述四类对象维护的资源,生命周期以及关键处理流程。

schedt对象

schedt对象代表整个调度框架,以全局变量的形式存在,其维护以下关键资源(详见runtime2.go文件中schedt结构体定义):

  • midle:空闲的m(实际上是m链表)
  • mnext:下一个m
  • pidle:空闲的p(实际上是p链表)
  • runq:全局可运行g队列
  • gFree:全局死亡的g
  • freem:待释放的m
schedt对象的创建

schedt对象是一个全局变量,在进程启动初始化阶段完成初始化,初始化函数的schedinit()。schedt对象的生命周期和进程的生命周期相同,不存在主动释放过程。

schedt对象更多起到容器的功能,存放各种资源,在调度过程中,并不参与实际的调度。

M对象

m对象代表一个调度器,其维护以下关键资源(详见runtime2.go文件中m结构体定义):

  • g0:m绑定的第0个goroutine
  • tls:线程级本地存储
  • curg:当前调度的goroutine
  • p:当前绑定的p
  • nextp:下一个可以调度的p
m对象的创建

创建m对象有如下4种场景:

  • 进程启动线程作为m0;
  • 进程启动过程中,创建监控m,执行sysmon函数;
  • 在创建g时,如果没有空闲的m,则创建一个m;
  • cgo用到的模板m;

m对象的创建函数是newm,在创建m的过程中会创建一个操作系统线程和一个默认的协程g0。m绑定的操作系统线程的入口函数为mstart,对于监控m和模板m,将分别死循环执行sysmon()templateThread()函数;其它创建的m调用这个函数后将周而复始调用shcedule()函数实现对可运行协程的调度(详细过程可以查看后续的代码解析-创建m章节)。

m对象的休眠

停止m对象有如下5种场景:

  • 锁定m绑定的操作系统线程
  • 当进程处于gc等待阶段
  • 当网络监控器无连接事件
  • 无可运行的g
  • 系统调用退出

停止m对象调用的是stopm (),在该函数中,m将被放入schedt对象的midle链中,同时调用mPark休眠直到被唤醒。

m对象的唤醒

对于停止的m,在如下场景下将被唤醒:

  • gc结束,重启所有的m
  • 为p启动m,当sched.midle有空闲的m

唤醒m对象调用的是notewakeup()函数,在该函数中,将设置m的停止标志key为1,mPark函数退出休眠,m被唤醒。

m对象的退出

只有在异常情况下m才会退出。

P对象

代表processor,维护以下关键资源:

  • mcache:缓存
  • m:当前绑定的m
  • runq:本地可运行g队列
  • runnext:下一个可运行的g
  • gFree:处于Gdead状态的g
p对象的创建

p对象在进程启动初始化阶段的schedinit()函数创建好,保存在allp全局变量中,p的个数有当前所在容器的CPU核数和设置的MAXPROCS数值决定。

p对象的休眠

p对象在以下场景下被休眠:

  • 当m退出或者停止时,m上绑定的p将休眠
  • gc阶段,将处于系统调用的p休眠

休眠p调用的是函数handoffp(),休眠的前提是该p上的可运行g队列为空。休眠的p将被存入sched对象的pidle链表中,等待被唤醒。

p对象的唤醒

p对象在以下场景下被唤醒:

  • g对象被唤醒
  • gc结束
  • 创建一个g对象
  • 调度非寻常goroutine
  • 启动m对象

唤醒p将从sched对象的pidle链表获取一个p对象,并将p对象绑定到m对象,等待调度。

g对象

g对象代表goroutine,维护以下关键资源:

代表goroutine,维护以下关键资源:

  • stack:协程栈
  • sched:用于保存协程调度的上下文
g对象的创建

goroutine在以下5种情况下创建:

  • 进程启动过程中创建的g0

  • 每创建一个m,会自动创建一个g0

  • 进程启动过程中用于执行main函数的g

  • 进程启动过程中,创建的背景扫描和清理g,用于gc

  • 用户创建的goroutine

在汇编代码中,g对象是通过call runtime·newproc创建的,在go代码中,则是通过go func()创建的(go是一个内建标识符,编译成汇编也是调用的runtime·newproc)。

g对象的休眠

goroutine在以下场景下休眠:

  • 读写channel阻塞时
  • 调用sleep时
  • 创建goroutine,并主动休眠等待被调度
  • 调用select时阻塞

休眠goroutine调用的是gopark()函数,在gopark()函数中,将切换到g0协程,并执行schedule()继续调度其它协程。

g对象的唤醒

goroutine在以下场景下被唤醒:

  • channel有数据或者空间可以读写
  • sleep时间到
  • 创建goroutine,并主动休眠等待被调度
  • select信号到达

唤醒goroutine调用的是goready()函数,在goready()函数中将再次将goroutine放到本地可运行g队列的next位置,等待调度,并唤醒所在的p。

g对象的退出

当goroutine执行完后,goroutine将退出,调度框架将m切换到g0,并继续执行schedule()继续调度其它协程。

调度过程

初始化

在进程启动初始化过程中,完成以下操作:

  1. 将引导进程启动的协程设置为全局g0,将引导进程启动的线程设置为全局m0,g0的调度m设置成m0;
  2. 初始化p对象,并将g0的调度m(即m0)的p设置成allp[0];
  3. 初始化sched对象,创建全局可运行队列,并将所有的p加入pidle队列中;
  4. 创建运行runtime·main函数(该函数调用用户的main函数)的goroutine,并设置为g0所在p的runnext,等待调度;
  5. 启动m0,进入循环调度schedule()状态;
  6. 首先调度runtime·main的协程,在该协程执行函数中将创建系统监控sysmon,同时创建背景gc使用的sweep和scavenge协程,然后调用用户定义的main函数(一般是一直阻塞)。

经过初始化过程后,运行时调度器(抛开用户main函数的执行)的状态如下图所示。

image-20220508212809998

循环调度

m在循环调度过程中,调度优先级冲高到低为:绑定的p的runnext,绑定的p的本地可运行队列队首的g,全局可运行队列队首的g,网络监控器(netpoll)中当前处于就绪状态的g,其它p的本地可运行队列中的g,gc状态下的gc worker的g。如果选择到合适的g,则调用execute()执行该协程,否则休眠当前的p和m。

当调度的g执行完成后,m再次调用schedule()调度下一个g,如此循环调度。

创建协程

main函数启动执行后,如果使用go创建协程时,默认情况(非随机调度)下,新建的协程会被设置成当前执行goroutine的m所绑定的p的runnext,等待优先调度,并将旧的runnext的goroutine放到本地可运行队列的尾部。如果是随机调度,新建的goroutine将随机放到本地可运行队列的尾部或者被设置成runnext。

当p的本地可运行队列满(装载的g达到256个),则将本地可运行队列中一般的g搬移到全局可运行队列。

接下来唤醒p,唤醒p的过程中,如果sched上没有空闲的p,则不做任何处理,否则调用starm()启动m。在starm()函数中,优先唤醒处于休眠状态的m,如果不存在休眠的m,则新建m。唤醒或者新建的m再次进入循环调度状态。

上述初步描述了go的GMP调度模型,相关的代码的解读将在本章后续小节中描述。