三足鼎立 —— GPM 到底是什么?(一)
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 liblink stackguard1 uintptr // offset known to liblink _panic *_panic // innermost panic - offset known to liblink _defer *_defer // innermost defer // 當前與 g 綁定的 m m *m // current m; offset known to arm liblink // goroutine 的運行現場 sched gobuf syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc stktopsp uintptr // expected sp at top of stack, to check in traceback // wakeup 時傳入的參數 param unsafe.Pointer // passed parameter on wakeup atomicstatus uint32 stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus goid int64 // g 被阻塞之后的近似時間 waitsince int64 // approx time when the g become blocked // g 被阻塞的原因 waitreason string // if status==Gwaiting // 指向全局隊列里下一個 g schedlink guintptr // 搶占調度標志。這個為 true 時,stackguard0 等于 stackpreempt preempt bool // preemption signal, duplicates stackguard0 = stackpreempt paniconfault bool // panic (instead of crash) on unexpected fault address preemptscan bool // preempted g does scan for gc gcscandone bool // g has scanned stack; protected by _Gscan bit in status gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove? throwsplit bool // must not split stack raceignore int8 // ignore race detection events sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine // syscall 返回之后的 cputicks,用來做 tracing sysexitticks int64 // cputicks when syscall has returned (for tracing) traceseq uint64 // trace event sequencer tracelastp puintptr // last P emitted an event for this goroutine // 如果調用了 LockOsThread,那么這個 g 會綁定到某個 m 上 lockedm *m sig uint32 writebuf []byte sigcode0 uintptr sigcode1 uintptr sigpc uintptr // 創建該 goroutine 的語句的指令地址 gopc uintptr // pc of go statement that created this goroutine // goroutine 函數的指令地址 startpc uintptr // pc of goroutine function racectx uintptr waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order cgoCtxt []uintptr // cgo traceback context labels unsafe.Pointer // profiler labels // time.Sleep 緩存的定時器 timer *timer // cached timer for time.Sleep gcAssistBytes 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 morestack divmod uint32 // div/mod denominator for arm - known to liblink // Fields not known to debuggers. procid uint64 // for debuggers, but offset not hard-coded gsignal *g // signal-handling g sigmask 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 goroutine caughtsig guintptr // goroutine running during fatal signal // 當前工作線程綁定的 p p puintptr // attached p for executing go code (nil if not executing go code) nextp puintptr id int32 mallocing int32 throwing int32 // 該字段不等于空字符串的話,要保持 curg 始終在這個 m 上運行 preemptoff string // if != "", keep curg running on this m locks int32 softfloat int32 dying int32 profilehz int32 helpgc 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 barrier inwb bool // m is executing a write barrier newSigstack bool // minit on C thread called sigaltstack printlock int8 // 正在執行 cgo 調用 incgo bool // m is executing a cgo call fastrand uint32 // cgo 調用總計數 ncgocall uint64 // number of cgo calls in total ncgo int32 // number of cgo calls currently in progress cgoCallersUse uint32 // if non-zero, cgoCallers in use temporarily cgoCallers *cgoCallers // cgo traceback if crashing in cgo call // 沒有 goroutine 需要運行時,工作線程睡眠在這個 park 成員上, // 其它線程通過這個 park 喚醒該工作線程 park note // 記錄所有工作線程的鏈表 alllink *m // on allm schedlink muintptr mcache *mcache lockedg *g createstack [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 flags locked uint32 // tracking for lockosthread // 正在等待鎖的下一個 m nextwaitm uintptr // next m waiting for lock needextram bool traceback uint8 waitunlockf unsafe.Pointer // todo go func(*g, unsafe.pointer) bool waitlock unsafe.Pointer waittraceev byte waittraceskip int startingtrace bool syscalltick uint32 // 工作線程 id thread uintptr // thread handle // these are here because they are too large to be on the stack // of low-level NOSPLIT functions. libcall libcall libcallpc uintptr // for cpu profiler libcallsp uintptr libcallg guintptr syscall libcall // stores syscall parameters on windows mOS }再來看 P,取 processor 的首字母,為 M 的執行提供“上下文”,保存 M 執行 G 時的一些資源,例如本地可運行 G 隊列,memeory cache 等。
一個 M 只有綁定 P 才能執行 goroutine,當 M 被阻塞時,整個 P 會被傳遞給其他 M ,或者說整個 P 被接管。
// p 保存 go 運行時所必須的資源 type p struct { lock mutex // 在 allp 中的索引 id int32 status uint32 // one of pidle/prunning/... link puintptr // 每次調用 schedule 時會加一 schedtick uint32 // 每次系統調用時加一 syscalltick uint32 // 用于 sysmon 線程記錄被監控 p 的系統調用時間和運行時間 sysmontick sysmontick // last tick observed by sysmon // 指向綁定的 m,如果 p 是 idle 的話,那這個指針是 nil m muintptr // back-link to associated m (nil if idle) mcache *mcache racectx uintptr deferpool [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 uint64 goidcacheend 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) // 空閑的 g gfree *g gfreecnt int32 sudogcache []*sudog sudogbuf [128]*sudog tracebuf traceBufPtr traceSwept, traceReclaimed uintptr palloc persistentAlloc // per-P to avoid mutex // Per-P GC state gcAssistTime int64 // Nanoseconds in assistAlloc gcBgMarkWorker guintptr gcMarkWorkerMode gcMarkWorkerMode runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point pad [sys.CacheLineSize]byte }GPM 三足鼎力,共同成就 Go scheduler。G 需要在 M 上才能運行,M 依賴 P 提供的資源,P 則持有待運行的 G。你中有我,我中有你。
借用曹大 golang notes 的一幅圖,描述三者的關系:
M 會從與它綁定的 P 的本地隊列獲取可運行的 G,也會從 network poller 里獲取可運行的 G,還會從其他 P 偷 G。
參考資料
【阿波張調度器系列教程】http://mp.weixin.qq.com/mp/homepage?_biz=MzU1OTg5NDkzOA==&hid=1&sn=8fc2b63f53559bc0cee292ce629c4788&scene=18#wechatredirect
總結
以上是生活随笔為你收集整理的三足鼎立 —— GPM 到底是什么?(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开天辟地 —— Go scheduler
- 下一篇: 生生世世 —— schedule 的轮回