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的状态有以下几种:
状态描述_Gidle
0刚刚被分配并且还没有被初始化_Grunnable
1没有执行代码,没有栈的所有权,存储在运行队列中_Grunning
2
可以执行代码,拥有栈的所有权,被赋予了内核线程M和处理器P_Gsyscall
3
正在执行系统调用,没有执行用户代码,拥有栈的所有权,被赋予了内核线程M但是不在运行队列上_Gwaiting
4
由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于Channel
的等待队列上。若需要时执行ready()唤醒。_Gmoribund_unused
5当前此状态未使用,但硬编码在了gdb
脚本里,可以不用关注_Gdead
6
没有被使用,可能刚刚退出,或在一个freelist;也或者刚刚被初始化;没有执行代码,可能有分配的栈也可能没有;G和分配的栈(如果已分配过栈)归刚刚退出G的M所有或从free
list中获取_Genqueue_unused
7目前未使用,不用理会_Gcopystack
8
栈正在被拷贝,没有执行代码,不在运行队列上_Gpreempted
9由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒_Gscan
10
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
}
-
waitsince
G阻塞时长。 -
waitreason
阻塞原因。typegstruct{
//asyncSafePointissetifgisstoppedatanasynchronous //safepoint.Thismeansthereareframesonthestack //withoutprecisepointerinformation. asyncSafePointbool paniconfaultbool//panic(insteadofcrash)onunexpectedfaultaddress gcscandonebool//ghasscannedstack;protectedby_Gscanbitinstatus throwsplitbool//mustnotsplitstack
}
-
asyncSafePoint
异步安全点;如果g在异步安全点
停止则设置为true
,表示在栈上没有精确的指针信息。 -
paniconfault
地址异常引起的panic(代替了崩溃)。 -
gcscandone
g扫描完了栈,受状态_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。 -
startpc
gofunc的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
状态以外的其它状态组合)。