G、P、M是Go调度器的三个核心组件,各司其职。在它们精密地配合下,Go调度器得以高效运转,这也是Go天然支持高并发的内在动力。今天这篇文章我们来深入理解GPM模型。
先看G,取goroutine的首字母,主要保存goroutine的一些状态信息以及CPU的一些寄存器的值,例如IP寄存器,以便在轮到本goroutine执行时,CPU知道要从哪一条指令处开始执行。
当goroutine被调离CPU时,调度器负责把CPU寄存器的值保存在g对象的成员变量之中。
当goroutine被调度起来运行时,调度器又负责把g对象的成员变量所保存的寄存器值恢复到CPU的寄存器。
本系列使用的代码版本是1.9.2,来看一下g的源码:
typegstruct{
//goroutine使用的栈
stack stack //offsetknowntoruntime/cgo
//用于栈的扩张和收缩检查,抢占标志
stackguard0uintptr//offsetknowntoliblink
stackguard1uintptr//offsetknowntoliblink
_panic *_panic//innermostpanic-offsetknowntoliblink
_defer *_defer//innermostdefer
//当前与g绑定的m
m *m //currentm;offsetknowntoarmliblink
//goroutine的运行现场
sched gobuf
syscallsp uintptr //ifstatus==Gsyscall,syscallsp=sched.sptouseduringgc
syscallpc uintptr //ifstatus==Gsyscall,syscallpc=sched.pctouseduringgc
stktopsp uintptr //expectedspattopofstack,tocheckintraceback
//wakeup时传入的参数
param unsafe.Pointer//passedparameteronwakeup
atomicstatus uint32
stackLock uint32//sigprof/scanglock;TODO:foldintoatomicstatus
goid int64
//g被阻塞之后的近似时间
waitsince int64 //approxtimewhenthegbecomeblocked
//g被阻塞的原因
waitreason string//ifstatus==Gwaiting
//指向全局队列里下一个g
schedlink guintptr
//抢占调度标志。这个为true时,stackguard0等于stackpreempt
preempt bool //preemptionsignal,duplicatesstackguard0=stackpreempt
paniconfault bool //panic(insteadofcrash)onunexpectedfaultaddress
preemptscan bool //preemptedgdoesscanforgc
gcscandone bool //ghasscannedstack;protectedby_Gscanbitinstatus
gcscanvalid bool //falseatstartofgccycle,trueifGhasnotrunsincelastscan;TODO:remove?
throwsplit bool //mustnotsplitstack
raceignore int8 //ignoreracedetectionevents
sysblocktracedbool //StartTracehasemittedEvGoInSyscallaboutthisgoroutine
//syscall返回之后的cputicks,用来做tracing
sysexitticks int64 //cputickswhensyscallhasreturned(fortracing)
traceseq uint64 //traceeventsequencer
tracelastp puintptr//lastPemittedaneventforthisgoroutine
//如果调用了LockOsThread,那么这个g会绑定到某个m上
lockedm *m
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
//创建该goroutine的语句的指令地址
gopc uintptr//pcofgostatementthatcreatedthisgoroutine
//goroutine函数的指令地址
startpc uintptr//pcofgoroutinefunction
racectx uintptr
waiting *sudog //sudogstructuresthisgiswaitingon(thathaveavalidelemptr);inlockorder
cgoCtxt []uintptr //cgotracebackcontext
labels unsafe.Pointer//profilerlabels
//time.Sleep缓存的定时器
timer *timer //cachedtimerfortime.Sleep
gcAssistBytesint64
}
源码中,比较重要的字段我已经作了注释,其他未作注释的与调度关系不大或者我暂时也没有理解的。
g
结构体关联了两个比较简单的结构体,stack表示goroutine运行时的栈:
//描述栈的数据结构,栈的范围:[lo,hi)
typestackstruct{
//栈顶,低地址
louintptr
//栈低,高地址
hiuintptr
}
Goroutine运行时,光有栈还不行,至少还得包括PC,SP等寄存器,gobuf就保存了这些值:
typegobufstruct{
//存储rsp寄存器的值
sp uintptr
//存储rip寄存器的值
pc uintptr
//指向goroutine
g guintptr
ctxtunsafe.Pointer//thishastobeapointersothatgcscansit
//保存系统调用的返回值
ret sys.Uintreg
lr uintptr
bp uintptr//forGOEXPERIMENT=framepointer
}
再来看M,取machine的首字母,它代表一个工作线程,或者说系统线程。G需要调度到M上才能运行,M是真正工作的人。结构体m就是我们常说的M,它保存了M自身使用的栈信息、当前正在M上执行的G信息、与之绑定的P信息……
当M没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看networkpoller,试图执行gc任务,或者“偷”工作。
结构体m的源码如下:
//m代表工作线程,保存了自身使用的栈信息
typemstruct{
//记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用
//执行用户goroutine代码时,使用用户goroutine自己的栈,因此调度时会发生栈的切换
g0 *g //goroutinewithschedulingstack/
morebufgobuf //gobufargtomorestack
divmod uint32//div/moddenominatorforarm-knowntoliblink
//Fieldsnotknowntodebuggers.
procid uint64 //fordebuggers,butoffsetnothard-coded
gsignal *g //signal-handlingg
sigmask sigset //storageforsavedsignalmask
//通过tls结构体实现m与工作线程的绑定
//这里是线程本地存储
tls [6]uintptr//thread-localstorage(forx86externregister)
mstartfn func()
//指向正在运行的gorutine对象
curg *g //currentrunninggoroutine
caughtsig guintptr//goroutinerunningduringfatalsignal
//当前工作线程绑定的p
p puintptr//attachedpforexecutinggocode(nilifnotexecutinggocode)
nextp puintptr
id int32
mallocing int32
throwing int32
//该字段不等于空字符串的话,要保持curg始终在这个m上运行
preemptoff string//if!="",keepcurgrunningonthism
locks int32
softfloat int32
dying int32
profilehz int32
helpgc int32
//为true时表示当前m处于自旋状态,正在从其他线程偷工作
spinning bool//misoutofworkandisactivelylookingforwork
//m正阻塞在note上
blocked bool//misblockedonanote
//m正在执行writebarrier
inwb bool//misexecutingawritebarrier
newSigstack bool//minitonCthreadcalledsigaltstack
printlock int8
//正在执行cgo调用
incgo bool//misexecutingacgocall
fastrand uint32
//cgo调用总计数
ncgocall uint64 //numberofcgocallsintotal
ncgo int32 //numberofcgocallscurrentlyinprogress
cgoCallersUseuint32 //ifnon-zero,cgoCallersinusetemporarily
cgoCallers *cgoCallers//cgotracebackifcrashingincgocall
//没有goroutine需要运行时,工作线程睡眠在这个park成员上,
//其它线程通过这个park唤醒该工作线程
park note
//记录所有工作线程的链表
alllink *m//onallm
schedlink muintptr
mcache *mcache
lockedg *g
createstack [32]uintptr//stackthatcreatedthisthread.
freglo [16]uint32 //d[i]lsbandf[i]
freghi [16]uint32 //d[i]msbandf[i+16]
fflag uint32 //floatingpointcompareflags
locked uint32 //trackingforlockosthread
//正在等待锁的下一个m
nextwaitm uintptr //nextmwaitingforlock
needextram bool
traceback uint8
waitunlockf unsafe.Pointer//todogofunc(*g,unsafe.pointer)bool
waitlock unsafe.Pointer
waittraceev byte
waittraceskipint
startingtracebool
syscalltick uint32
//工作线程id
thread uintptr//threadhandle
//theseareherebecausetheyaretoolargetobeonthestack
//oflow-levelNOSPLITfunctions.
libcall libcall
libcallpcuintptr//forcpuprofiler
libcallspuintptr
libcallg guintptr
syscall libcall//storessyscallparametersonwindows
mOS
}
再来看P,取processor的首字母,为M的执行提供“上下文”,保存M执行G时的一些资源,例如本地可运行G队列,memeorycache等。
一个M只有绑定P才能执行goroutine,当M被阻塞时,整个P会被传递给其他M,或者说整个P被接管。
//p保存go运行时所必须的资源
typepstruct{
lockmutex
//在allp中的索引
id int32
status uint32//oneofpidle/prunning/...
link puintptr
//每次调用schedule时会加一
schedtick uint32
//每次系统调用时加一
syscalltickuint32
//用于sysmon线程记录被监控p的系统调用时间和运行时间
sysmontick sysmontick//lasttickobservedbysysmon
//指向绑定的m,如果p是idle的话,那这个指针是nil
m muintptr //back-linktoassociatedm(nilifidle)
mcache *mcache
racectx uintptr
deferpool [5][]*_defer//poolofavailabledeferstructsofdifferentsizes(seepanic.go)
deferpoolbuf[5][32]*_defer
//Cacheofgoroutineids,amortizesaccessestoruntime·sched.goidgen.
goidcache uint64
goidcacheenduint64
//Queueofrunnablegoroutines.Accessedwithoutlock.
//本地可运行的队列,不用通过锁即可访问
runqheaduint32//队列头
runqtailuint32//队列尾
//使用数组实现的循环队列
runq [256]guintptr
//runnext非空时,代表的是一个runnable状态的G,
//这个G被当前G修改为ready状态,相比runq中的G有更高的优先级。
//如果当前G还有剩余的可用时间,那么就应该运行这个G
//运行之后,该G会继承当前G的剩余时间
runnextguintptr
//AvailableG's(status==Gdead)
//空闲的g
gfree *g
gfreecntint32
sudogcache[]*sudog
sudogbuf [128]*sudog
tracebuftraceBufPtr
traceSwept,traceReclaimeduintptr
pallocpersistentAlloc//per-Ptoavoidmutex
//Per-PGCstate
gcAssistTime int64//NanosecondsinassistAlloc
gcBgMarkWorker guintptr
gcMarkWorkerModegcMarkWorkerMode
runSafePointFnuint32//if1,runsched.safePointFnatnextsafepoint
pad[sys.CacheLineSize]byte
}
GPM三足鼎力,共同成就Goscheduler。G需要在M上才能运行,M依赖P提供的资源,P则持有待运行的G。你中有我,我中有你。
描述三者的关系:
M会从与它绑定的P的本地队列获取可运行的G,也会从networkpoller里获取可运行的G,还会从其他P偷G。
最后我们从宏观上总结一下GPM,这篇文章尝试从它们的状态流转角度总结。
首先是G的状态流转:
说明一下,上图省略了一些垃圾回收的状态。
接着是P的状态流转:
通常情况下(在程序运行时不调整P的个数),P只会在上图中的四种状态下进行切换。当程序刚开始运行进行初始化时,所有的P都处于
_Pgcstop
状态,随着P的初始化(runtime.procresize
),会被置于_Pidle
。
当M需要运行时,会
runtime.acquirep
来使P变成Prunning
状态,并通过runtime.releasep
来释放。
当G执行时需要进入系统调用,P会被设置为
_Psyscall
,如果这个时候被系统监控抢夺(runtime.retake
),则P会被重新修改为_Pidle
。
如果在程序运行中发生
GC
,则P会被设置为_Pgcstop
,并在runtime.startTheWorld
时重新调整为_Prunning
。
最后,我们来看M的状态变化:
M只有自旋和非自旋两种状态。自旋的时候,会努力找工作;找不到的时候会进入非自旋状态,之后会休眠,直到有工作需要处理时,被其他工作线程唤醒,又进入自旋状态。
本文节选于Go合集《Go 语言问题集》:GOLANG ROADMAP 一个专注Go语言学习、求职的社区。