G、P、M 是 Go 调度器的三个核心组件,各司其职。在它们精密地配合下,Go 调度器得以高效运转,这也是 Go 天然支持高并发的内在动力。今天这篇文章我们来深入理解 GPM 模型。
先看 G,取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。
当 goroutine 被调离 CPU 时,调度器负责把 CPU 寄存器的值保存在 g 对象的成员变量之中。
当 goroutine 被调度起来运行时,调度器又负责把 g 对象的成员变量所保存的寄存器值恢复到 CPU 的寄存器。
上面这段描述来自公众号“go语言核心编程技术”的调度器系列文章,写得非常好,推荐大家去看,参考资料【阿波张调度器系列教程】可以到达原文。
本系列教程使用的代码版本是 1.9.2,来看一下 g 的源码:
type g struct {// goroutine 使用的栈stack stack // offset known to runtime/cgo// 用于栈的扩张和收缩检查,抢占标志stackguard0 uintptr // offset known to liblinkstackguard1 uintptr // offset known to liblink_panic *_panic // innermost panic - offset known to liblink_defer *_defer // innermost defer// 当前与 g 绑定的 mm *m // current m; offset known to arm liblink// goroutine 的运行现场sched gobufsyscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gcsyscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gcstktopsp uintptr // expected sp at top of stack, to check in traceback// wakeup 时传入的参数param unsafe.Pointer // passed parameter on wakeupatomicstatus uint32stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatusgoid int64// g 被阻塞之后的近似时间waitsince int64 // approx time when the g become blocked// g 被阻塞的原因waitreason string // if status==Gwaiting// 指向全局队列里下一个 gschedlink guintptr// 抢占调度标志。这个为 true 时,stackguard0 等于 stackpreemptpreempt bool // preemption signal, duplicates stackguard0 = stackpreemptpaniconfault bool // panic (instead of crash) on unexpected fault addresspreemptscan bool // preempted g does scan for gcgcscandone bool // g has scanned stack; protected by _Gscan bit in statusgcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove?throwsplit bool // must not split stackraceignore int8 // ignore race detection eventssysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine// syscall 返回之后的 cputicks,用来做 tracingsysexitticks int64 // cputicks when syscall has returned (for tracing)traceseq uint64 // trace event sequencertracelastp puintptr // last P emitted an event for this goroutine// 如果调用了 LockOsThread,那么这个 g 会绑定到某个 m 上lockedm *msig uint32writebuf []bytesigcode0 uintptrsigcode1 uintptrsigpc uintptr// 创建该 goroutine 的语句的指令地址gopc uintptr // pc of go statement that created this goroutine// goroutine 函数的指令地址startpc uintptr // pc of goroutine functionracectx uintptrwaiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock ordercgoCtxt []uintptr // cgo traceback contextlabels unsafe.Pointer // profiler labels// time.Sleep 缓存的定时器timer *timer // cached timer for time.SleepgcAssistBytes int64}
源码中,比较重要的字段我已经作了注释,其他未作注释的与调度关系不大或者我暂时也没有理解的。
g 结构体关联了两个比较简单的结构体,stack 表示 goroutine 运行时的栈:
// 描述栈的数据结构,栈的范围:[lo, hi)type stack struct { // 栈顶,低地址 lo uintptr // 栈低,高地址 hi uintptr}Goroutine 运行时,光有栈还不行,至少还得包括 PC,SP 等寄存器,gobuf 就保存了这些值:
type gobuf struct { // 存储 rsp 寄存器的值 sp uintptr // 存储 rip 寄存器的值 pc uintptr // 指向 goroutine g guintptr ctxt unsafe.Pointer // this has to be a pointer so that gc scans it // 保存系统调用的返回值 ret sys.Uintreg lr uintptr bp uintptr // for GOEXPERIMENT=framepointer}再来看 M,取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。结构体 m 就是我们常说的 M,它保存了 M 自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息……
当 M 没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看 network poller,试图执行 gc 任务,或者“偷”工作。
结构体 m 的源码如下:
// m 代表工作线程,保存了自身使用的栈信息type m struct {// 记录工作线程(也就是内核线程)使用的栈信息。在执行调度代码时需要使用// 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换g0 *g // goroutine with scheduling stack/morebuf gobuf // gobuf arg to morestackdivmod uint32 // div/mod denominator for arm - known to liblink// Fields not known to debuggers.procid uint64 // for debuggers, but offset not hard-codedgsignal *g // signal-handling gsigmask sigset // storage for saved signal mask// 通过 tls 结构体实现 m 与工作线程的绑定// 这里是线程本地存储tls [6]uintptr // thread-local storage (for x86 extern register)mstartfn func()// 指向正在运行的 gorutine 对象curg *g // current running goroutinecaughtsig guintptr // goroutine running during fatal signal// 当前工作线程绑定的 pp puintptr // attached p for executing go code (nil if not executing go code)nextp puintptrid int32mallocing int32throwing int32// 该字段不等于空字符串的话,要保持 curg 始终在这个 m 上运行preemptoff string // if != "", keep curg running on this mlocks int32softfloat int32dying int32profilehz int32helpgc int32// 为 true 时表示当前 m 处于自旋状态,正在从其他线程偷工作spinning bool // m is out of work and is actively looking for work// m 正阻塞在 note 上blocked bool // m is blocked on a note// m 正在执行 write barrierinwb bool // m is executing a write barriernewSigstack bool // minit on C thread called sigaltstackprintlock int8// 正在执行 cgo 调用incgo bool // m is executing a cgo callfastrand uint32// cgo 调用总计数ncgocall uint64 // number of cgo calls in totalncgo int32 // number of cgo calls currently in progresscgoCallersUse uint32 // if non-zero, cgoCallers in use temporarilycgoCallers *cgoCallers // cgo traceback if crashing in cgo call// 没有 goroutine 需要运行时,工作线程睡眠在这个 park 成员上,// 其它线程通过这个 park 唤醒该工作线程park note// 记录所有工作线程的链表alllink *m // on allmschedlink muintptrmcache *mcachelockedg *gcreatestack [32]uintptr // stack that created this thread.freglo [16]uint32 // d[i] lsb and f[i]freghi [16]uint32 // d[i] msb and f[i+16]fflag uint32 // floating point compare flagslocked uint32 // tracking for lockosthread// 正在等待锁的下一个 mnextwaitm uintptr // next m waiting for lockneedextram booltraceback uint8waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) boolwaitlock unsafe.Pointerwaittraceev bytewaittraceskip intstartingtrace boolsyscalltick uint32// 工作线程 idthread uintptr // thread handle// these are here because they are too large to be on the stack// of low-level NOSPLIT functions.libcall libcalllibcallpc uintptr // for cpu profilerlibcallsp uintptrlibcallg guintptrsyscall libcall // stores syscall parameters on windowsmOS}
再来看 P,取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。
一个 M 只有绑定 P 才能执行 goroutine,当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管。
// p 保存 go 运行时所必须的资源type p struct {lock mutex// 在 allp 中的索引id int32status uint32 // one of pidle/prunning/...link puintptr// 每次调用 schedule 时会加一schedtick uint32// 每次系统调用时加一syscalltick uint32// 用于 sysmon 线程记录被监控 p 的系统调用时间和运行时间sysmontick sysmontick // last tick observed by sysmon// 指向绑定的 m,如果 p 是 idle 的话,那这个指针是 nilm muintptr // back-link to associated m (nil if idle)mcache *mcacheracectx uintptrdeferpool [5][]*_defer // pool of available defer structs of different sizes (see panic.go)deferpoolbuf [5][32]*_defer// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.goidcache uint64goidcacheend uint64// Queue of runnable goroutines. Accessed without lock.// 本地可运行的队列,不用通过锁即可访问runqhead uint32 // 队列头runqtail uint32 // 队列尾// 使用数组实现的循环队列runq [256]guintptr// runnext 非空时,代表的是一个 runnable 状态的 G,// 这个 G 被 当前 G 修改为 ready 状态,相比 runq 中的 G 有更高的优先级。// 如果当前 G 还有剩余的可用时间,那么就应该运行这个 G// 运行之后,该 G 会继承当前 G 的剩余时间runnext guintptr// Available G's (status == Gdead)// 空闲的 ggfree *ggfreecnt int32sudogcache []*sudogsudogbuf [128]*sudogtracebuf traceBufPtrtraceSwept, traceReclaimed uintptrpalloc persistentAlloc // per-P to avoid mutex// Per-P GC stategcAssistTime int64 // Nanoseconds in assistAllocgcBgMarkWorker guintptrgcMarkWorkerMode gcMarkWorkerModerunSafePointFn uint32 // if 1, run sched.safePointFn at next safe pointpad [sys.CacheLineSize]byte}
GPM 三足鼎力,共同成就 Go scheduler。G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G。你中有我,我中有你。
借用曹大 golang notes 的一幅图,描述三者的关系:

M 会从与它绑定的 P 的本地队列获取可运行的 G,也会从 network poller 里获取可运行的 G,还会从其他 P 偷 G。
随时随地看视频