GPM 是什么?

GPM 是什么?go 语言里的 GPM 是什么 gpm

大家好,欢迎来到IT知识分享网。

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语言学习、求职的社区。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/148947.html

(0)
上一篇 2025-03-27 14:45
下一篇 2025-03-27 15:00

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信