首页 文章资讯内容详情

Go语言并发模型 G源码分析

2026-06-01 3 花语

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,whereits //difficulttohavewritebarriers.However,ctxtisreallya //saved,liveregister,andweonlyeverexchangeitbetween //therealregisterandthegobuf.Hence,wetreatitasa //rootduringstackscanning,whichmeansassemblythatsaves //andrestoresitdoesntneedwritebarriers.Itsstill //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 //pointingintothisgoroutinesstack.Iftrue,stack //copyingneedstoacquirechannellockstoprotectthese //areasofthestack. activeStackChansbool //parkingOnChanindicatesthatthegoroutineisaboutto //parkonachansendorchanrecv.Usedtosignalanunsafepoint //forstackshrinking.Itsabooleanvalue,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 //gcAssistBytesisthisGsGCassistcreditintermsof //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状态以外的其它状态组合)。 原文链接: