Go语言并发模型 G源码分析
Go的线程实现模型,有三个核心的元素M、P、G,它们共同支撑起了这个线程模型的框架。其中,G是goroutine的缩写,通常称为“协程”。关于协程、线程和进程三者的异同,可以参照“进程、线程和协程的区别”。
每一个Goroutine在程序运行期间,都会对应分配一个g结构体对象。g中存储着Goroutine的运行堆栈、状态以及任务函数,g结构的定义位于src/runtime/runtime2.go文件中。
g对象可以重复使用,当一个goroutine退出时,g对象会被放到一个空闲的g对象池中以用于后续的goroutine
的使用,以减少内存分配开销。
1.Goroutine字段注释
g字段非常的多,我们这里分段来理解:
​
typegstruct{
//Stackparameters.
//stackdescribestheactualstackmemory:[stack.lo,stack.hi).
//stackguard0isthestackpointercomparedintheGostackgrowthprologue.
//Itisstack.lo+StackGuardnormally,butcanbeStackPreempttotriggerapreemption.
//stackguard1isthestackpointercomparedintheCstackgrowthprologue.
//Itisstack.lo+StackGuardong0andgsignalstacks.
//Itis~0onothergoroutinestacks,totriggeracalltomorestackc(andcrash).
stackstack//offsetknowntoruntime/cgo
​
//检查栈空间是否足够的值,低于这个值会扩张,stackguard0供Go代码使用
stackguard0uintptr//offsetknowntoliblink
​
//检查栈空间是否足够的值,低于这个值会扩张,stackguard1供C代码使用
stackguard1uintptr//offsetknowntoliblink
}
​
stack描述了当前goroutine的栈内存范围[stack.lo,stack.hi),其中stack的数据结构:
​
//StackdescribesaGoexecutionstack.
//Theboundsofthestackareexactly[lo,hi),
//withnoimplicitdatastructuresoneitherside.
//描述goroutine执行栈
//栈边界为[lo,hi),左包含右不包含,即lo≤stack<hi
//两边都没有隐含的数据结构。
typestackstruct{
louintptr//该协程拥有的栈低位
hiuintptr//该协程拥有的栈高位
}
​
stackguard0和stackguard1均是一个栈指针,用于扩容场景,前者用于Gostack,后者用于Cstack。
如果stackguard0字段被设置成StackPreempt,意味着当前Goroutine发出了抢占请求。
在g结构体中的stackguard0字段是出现爆栈前的警戒线。stackguard0的偏移量是16个字节,与当前的真实SP(stackpointer)和爆栈警戒线(stack.lo+StackGuard)比较,如果超出警戒线则表示需要进行栈扩容。先调用runtime·morestack_noctxt()进行栈扩容,然后又跳回到函数的开始位置,此时此刻函数的栈已经调整了。然后再进行一次栈大小的检测,如果依然不足则继续扩容,直到栈足够大为止。
​
typegstruct{
preemptbool//preemptionsignal,duplicatesstackguard0=stackpreempt
preemptStopbool//transitionto_Gpreemptedonpreemption;otherwise,justdeschedule
preemptShrinkbool//shrinkstackatsynchronoussafepoint
}
​
-
preempt抢占标记,其值为true执行stackguard0=stackpreempt。 -
preemptStop将抢占标记修改为_Gpreedmpted,如果修改失败则取消。 -
preemptShrink在同步安全点收缩栈。typegstruct{
_panic*_panic//innermostpanic-offsetknowntoliblink _defer*_defer//innermostdefer}
-
_panic当前Goroutine中的panic。 -
_defer当前Goroutine中的defer。typegstruct{
m*m//currentm;offsetknowntoarmliblink schedgobuf goidint64}
-
m当前Goroutine绑定的M。 -
sched存储当前Goroutine调度相关的数据,上下方切换时会把当前信息保存到这里,用的时候再取出来。 -
goid当前Goroutine的唯一标识,对开发者不可见,一般不使用此字段,Go开发团队未向外开放访问此字段。
gobuf结构体定义:
​
typegobufstruct{
//Theoffsetsofsp,pc,andgareknownto(hard-codedin)libmach.
//寄存器sp,pc和g的偏移量,硬编码在libmach
//
//ctxtisunusualwithrespecttoGC:itmaybea
//heap-allocatedfuncval,soGCneedstotrackit,butit
//needstobesetandclearedfromassembly,whereit's
//difficulttohavewritebarriers.However,ctxtisreallya
//saved,liveregister,andweonlyeverexchangeitbetween
//therealregisterandthegobuf.Hence,wetreatitasa
//rootduringstackscanning,whichmeansassemblythatsaves
//andrestoresitdoesn'tneedwritebarriers.It'sstill
//typedasapointersothatanyotherwritesfromGoget
//writebarriers.
spuintptr
pcuintptr
gguintptr
ctxtunsafe.Pointer
retsys.Uintreg
lruintptr
bpuintptr//forGOEXPERIMENT=framepointer
}
​
sp栈指针位置。pc程序计数器,运行到的程序位置。ctxt不常见,可能是一个分配在heap的函数变量,因此GC需要追踪它,不过它有可能需要设置并进行清除,在有写屏障的时候有些困难。重点了解一下writebarriers。g当前gobuf的Goroutine。ret系统调用的结果。
调度器在将G由一种状态变更为另一种状态时,需要将上下文信息保存到这个gobuf结构体,当再次运行G
的时候,再从这个结构体中读取出来,它主要用来暂存上下文信息。其中的栈指针sp和程序计数器pc会用来存储或者恢复寄存器中的值,设置即将执行的代码。
2.Goroutine状态种类
Goroutine的状态有以下几种:
状态描述_Gidle0刚刚被分配并且还没有被初始化_Grunnable1没有执行代码,没有栈的所有权,存储在运行队列中_Grunning2
可以执行代码,拥有栈的所有权,被赋予了内核线程M和处理器P_Gsyscall3
正在执行系统调用,没有执行用户代码,拥有栈的所有权,被赋予了内核线程M但是不在运行队列上_Gwaiting4
由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于Channel
的等待队列上。若需要时执行ready()唤醒。_Gmoribund_unused5当前此状态未使用,但硬编码在了gdb
脚本里,可以不用关注_Gdead6
没有被使用,可能刚刚退出,或在一个freelist;也或者刚刚被初始化;没有执行代码,可能有分配的栈也可能没有;G和分配的栈(如果已分配过栈)归刚刚退出G的M所有或从free
list中获取_Genqueue_unused7目前未使用,不用理会_Gcopystack8
栈正在被拷贝,没有执行代码,不在运行队列上_Gpreempted9由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒_Gscan10
GC正在扫描栈空间,没有执行代码,可以与其他状态同时存在
需要注意的是对于_Gmoribund_unused状态并未使用,但在gdb脚本中存在;而对于_Genqueue_unused
状态目前也未使用,不需要关心。
_Gscan与上面除了_Grunning状态以外的其它状态相组合,表示GC正在扫描栈。Goroutine不会执行用户代码,且栈由设置了
_Gscan位的Goroutine所有。
状态描述_Gscanrunnable=_Gscan+_Grunnable//0x1001_Gscanrunning=_Gscan+
_Grunning//0x1002_Gscansyscall=_Gscan+_Gsyscall//
0x1003_Gscanwaiting=_Gscan+_Gwaiting//0x1004_Gscanpreempted=_Gscan+
_Gpreempted//0x1009
3.Goroutine状态转换
可以看到除了上面提到的两个未使用的状态外一共有14种状态值。许多状态之间是可以进行改变的。如下图所示:
typegstrcut{
syscallspuintptr//ifstatus==Gsyscall,syscallsp=sched.sptouseduringgc
syscallpcuintptr//ifstatus==Gsyscall,syscallpc=sched.pctouseduringgc
stktopspuintptr//expectedspattopofstack,tocheckintraceback
paramunsafe.Pointer//passedparameteronwakeup
atomicstatusuint32
stackLockuint32//sigprof/scanglock;TODO:foldintoatomicstatus
}
​
-
atomicstatus当前G的状态,上面介绍过G的几种状态值。 -
syscallsp如果G的状态为Gsyscall,那么值为sched.sp主要用于GC期间。 -
syscallpc如果G的状态为GSyscall,那么值为sched.pc主要用于GC期间。由此可见这两个字段通常一起使用。 -
stktopsp用于回源跟踪。 -
param唤醒G时传入的参数,例如调用ready()。 -
stackLock栈锁。typegstruct{
waitsinceint64//approxtimewhenthegbecomeblocked waitreasonwaitReason//ifstatus==Gwaiting}
-
waitsinceG阻塞时长。 -
waitreason阻塞原因。typegstruct{
//asyncSafePointissetifgisstoppedatanasynchronous //safepoint.Thismeansthereareframesonthestack //withoutprecisepointerinformation. asyncSafePointbool paniconfaultbool//panic(insteadofcrash)onunexpectedfaultaddress gcscandonebool//ghasscannedstack;protectedby_Gscanbitinstatus throwsplitbool//mustnotsplitstack}
-
asyncSafePoint异步安全点;如果g在异步安全点停止则设置为true,表示在栈上没有精确的指针信息。 -
paniconfault地址异常引起的panic(代替了崩溃)。 -
gcscandoneg扫描完了栈,受状态_Gscan位保护。 -
throwsplit不允许拆分stack。typegstruct{
//activeStackChansindicatesthatthereareunlockedchannels //pointingintothisgoroutine'sstack.Iftrue,stack //copyingneedstoacquirechannellockstoprotectthese //areasofthestack. activeStackChansbool //parkingOnChanindicatesthatthegoroutineisaboutto //parkonachansendorchanrecv.Usedtosignalanunsafepoint //forstackshrinking.It'sabooleanvalue,butisupdatedatomically. parkingOnChanuint8}
-
activeStackChans表示是否有未加锁定的channel指向到了g栈,如果为true,那么对栈的复制需要channal锁来保护这些区域。 -
parkingOnChan表示g是放在chansend还是chanrecv。用于栈的收缩,是一个布尔值,但是原子性更新。typegstruct{
raceignoreint8//ignoreracedetectionevents sysblocktracedbool//StartTracehasemittedEvGoInSyscallaboutthisgoroutine sysexitticksint64//cputickswhensyscallhasreturned(fortracing) tracesequint64//traceeventsequencer tracelastppuintptr//lastPemittedaneventforthisgoroutine lockedmmuintptr siguint32 writebuf[]byte sigcode0uintptr sigcode1uintptr sigpcuintptr gopcuintptr//pcofgostatementthatcreatedthisgoroutine ancestors*[]ancestorInfo//ancestorinformationgoroutine(s)thatcreatedthisgoroutine(onlyusedifdebug.tracebackancestors) startpcuintptr//pcofgoroutinefunction racectxuintptr waiting*sudog//sudogstructuresthisgiswaitingon(thathaveavalidelemptr);inlockorder cgoCtxt[]uintptr//cgotracebackcontext labelsunsafe.Pointer//profilerlabels timer*timer//cachedtimerfortime.Sleep selectDoneuint32//areweparticipatinginaselectanddidsomeonewintherace?}
-
gopc创建当前G的pc。 -
startpcgofunc的pc。 -
timer通过time.Sleep缓存timer。typegstruct{
//Per-GGCstate //gcAssistBytesisthisG'sGCassistcreditintermsof //bytesallocated.Ifthisispositive,thentheGhascredit //toallocategcAssistBytesbyteswithoutassisting.Ifthis //isnegative,thentheGmustcorrectthisbyperforming //scanwork.Wetrackthisinbytestomakeitfasttoupdate //andcheckfordebtinthemallochotpath.Theassistratio //determineshowthiscorrespondstoscanworkdebt. gcAssistBytesint64}
-
gcAssistBytes与GC相关。
4.Goroutin总结
- 每个G都有自己的状态,状态保存在
atomicstatus字段,共有十几种状态值。 - 每个G在状态发生变化时,即
atomicstatus字段值被改变时,都需要保存当前G的上下文的信息,这个信息存储在sched字段,其数据类型为gobuf,想理解存储的信息可以看一下这个结构体的各个字段。 - 每个G都有三个与抢占有关的字段,分别为
preempt、preemptStop和premptShrink。 - 每个G都有自己的唯一id,字段为
goid,但此字段官方不推荐开发使用。 - 每个G都可以最多绑定一个m,如果可能未绑定,则值为nil。
- 每个G都有自己内部的
defer和panic。 - G可以被阻塞,并存储有阻塞原因,字段
waitsince和waitreason。 - G可以被进行GC扫描,相关字段为
gcscandone、atomicstatus(_Gscan与上面除了_Grunning状态以外的其它状态组合)。