在1.1版本后,Go采用GMP模型调度协程,其逻辑结构如下图所示。
调度模型由4类对象schedt,m,p,g组成,其中:
schedt对象:scheduler,整个进程一个实例,代表整个调度框架。
m对象:machine,每个m对象绑定一个操作系统线程,调度goroutine。
p对象:processor,其个数一般和进程所在容器(物理机,虚拟机,容器等)的核数相同,负责维护g对象和本地内存管理。
g对象:goroutine,程序最小执行单元,进程中有大量的goroutine,其数量一般来说没有上限。
正如上节讲到的通用的携程调度模型,在GMP模型中,goroutine被m周而复始的调度,但是在调度过程中运用了比较复杂的算法,以求达到最佳的调度效率。下面将详细讲解上述四类对象维护的资源,生命周期以及关键处理流程。
schedt对象代表整个调度框架,以全局变量的形式存在,其维护以下关键资源(详见runtime2.go文件中schedt结构体定义):
schedt对象是一个全局变量,在进程启动初始化阶段完成初始化,初始化函数的schedinit()
。schedt对象的生命周期和进程的生命周期相同,不存在主动释放过程。
schedt对象更多起到容器的功能,存放各种资源,在调度过程中,并不参与实际的调度。
m对象代表一个调度器,其维护以下关键资源(详见runtime2.go文件中m结构体定义):
创建m对象有如下4种场景:
m对象的创建函数是newm,在创建m的过程中会创建一个操作系统线程和一个默认的协程g0。m绑定的操作系统线程的入口函数为mstart,对于监控m和模板m,将分别死循环执行sysmon()
和templateThread()
函数;其它创建的m调用这个函数后将周而复始调用shcedule()
函数实现对可运行协程的调度(详细过程可以查看后续的代码解析-创建m章节)。
停止m对象有如下5种场景:
停止m对象调用的是stopm (),在该函数中,m将被放入schedt对象的midle链中,同时调用mPark休眠直到被唤醒。
对于停止的m,在如下场景下将被唤醒:
唤醒m对象调用的是notewakeup()
函数,在该函数中,将设置m的停止标志key为1,mPark函数退出休眠,m被唤醒。
只有在异常情况下m才会退出。
代表processor,维护以下关键资源:
p对象在进程启动初始化阶段的schedinit()
函数创建好,保存在allp
全局变量中,p的个数有当前所在容器的CPU核数和设置的MAXPROCS数值决定。
p对象在以下场景下被休眠:
休眠p调用的是函数handoffp()
,休眠的前提是该p上的可运行g队列为空。休眠的p将被存入sched对象的pidle链表中,等待被唤醒。
p对象在以下场景下被唤醒:
唤醒p将从sched对象的pidle链表获取一个p对象,并将p对象绑定到m对象,等待调度。
g对象代表goroutine,维护以下关键资源:
代表goroutine,维护以下关键资源:
goroutine在以下5种情况下创建:
进程启动过程中创建的g0
每创建一个m,会自动创建一个g0
进程启动过程中用于执行main函数的g
进程启动过程中,创建的背景扫描和清理g,用于gc
用户创建的goroutine
在汇编代码中,g对象是通过call runtime·newproc
创建的,在go代码中,则是通过go func()
创建的(go是一个内建标识符,编译成汇编也是调用的runtime·newproc)。
goroutine在以下场景下休眠:
休眠goroutine调用的是gopark()
函数,在gopark()
函数中,将切换到g0协程,并执行schedule()
继续调度其它协程。
goroutine在以下场景下被唤醒:
唤醒goroutine调用的是goready()
函数,在goready()函数中将再次将goroutine放到本地可运行g队列的next位置,等待调度,并唤醒所在的p。
当goroutine执行完后,goroutine将退出,调度框架将m切换到g0,并继续执行schedule()
继续调度其它协程。
在进程启动初始化过程中,完成以下操作:
runtime·main
函数(该函数调用用户的main函数)的goroutine,并设置为g0所在p的runnext,等待调度;schedule()
状态;runtime·main
的协程,在该协程执行函数中将创建系统监控sysmon,同时创建背景gc使用的sweep和scavenge协程,然后调用用户定义的main函数(一般是一直阻塞)。经过初始化过程后,运行时调度器(抛开用户main函数的执行)的状态如下图所示。
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调度模型,相关的代码的解读将在本章后续小节中描述。