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 欄位非常的多,我們這裡分段來理解:
type g struct { // Stack parameters. // stack describes the actual stack memory: [stack.lo, stack.hi). // stackguard0 is the stack pointer compared in the Go stack growth prologue. // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. // stackguard1 is the stack pointer compared in the C stack growth prologue. // It is stack.lo+StackGuard on g0 and gsignal stacks. // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). stack stack // offset known to runtime/cgo // 檢查棧空間是否足夠的值, 低於這個值會擴張, stackguard0 供 Go 程式碼使用 stackguard0 uintptr // offset known to liblink // 檢查棧空間是否足夠的值, 低於這個值會擴張, stackguard1 供 C 程式碼使用 stackguard1 uintptr // offset known to liblink }
stack
描述了當前goroutine
的棧記憶體範圍[stack.lo, stack.hi)
,其中 stack 的資料結構:
// Stack describes a Go execution stack. // The bounds of the stack are exactly [lo, hi), // with no implicit data structures on either side. // 描述 goroutine 執行棧 // 棧邊界為[lo, hi),左包含右不包含,即 lo≤stack<hi // 兩邊都沒有隱含的資料結構。 type stack struct { lo uintptr // 該協程擁有的棧低位 hi uintptr // 該協程擁有的棧高位 }
stackguard0
和stackguard1
均是一個棧指標,用於擴容場景,前者用於 Go stack ,後者用於 C stack。
如果stackguard0
欄位被設定成StackPreempt
,意味著當前 Goroutine 發出了搶佔請求。
在g
結構體中的stackguard0
欄位是出現爆棧前的警戒線。stackguard0
的偏移量是16
個位元組,與當前的真實SP(stack pointer)
和爆棧警戒線(stack.lo+StackGuard
)比較,如果超出警戒線則表示需要進行棧擴容。先呼叫runtime·morestack_noctxt()
進行棧擴容,然後又跳回到函式的開始位置,此時此刻函式的棧已經調整了。然後再進行一次棧大小的檢測,如果依然不足則繼續擴容,直到棧足夠大為止。
type g struct { preempt bool // preemption signal, duplicates stackguard0 = stackpreempt preemptStop bool // transition to _Gpreempted on preemption; otherwise, just deschedule preemptShrink bool // shrink stack at synchronous safe point }
preempt
搶佔標記,其值為 true 執行 stackguard0 = stackpreempt。preemptStop
將搶佔標記修改為 _Gpreedmpted,如果修改失敗則取消。preemptShrink
在同步安全點收縮棧。
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
_panic
當前Goroutine 中的 panic。_defer
當前Goroutine 中的 defer。
type g struct {
m *m // current m; offset known to arm liblink
sched gobuf
goid int64
}
m
當前 Goroutine 繫結的 M。sched
儲存當前 Goroutine 排程相關的資料,上下方切換時會把當前資訊儲存到這裡,用的時候再取出來。goid
當前 Goroutine 的唯一標識,對開發者不可見,一般不使用此欄位,Go 開發團隊未向外開放訪問此欄位。
gobuf 結構體定義:
type gobuf struct { // The offsets of sp, pc, and g are known to (hard-coded in) libmach. // 暫存器 sp, pc 和 g 的偏移量,硬編碼在 libmach // // ctxt is unusual with respect to GC: it may be a // heap-allocated funcval, so GC needs to track it, but it // needs to be set and cleared from assembly, where it's // difficult to have write barriers. However, ctxt is really a // saved, live register, and we only ever exchange it between // the real register and the gobuf. Hence, we treat it as a // root during stack scanning, which means assembly that saves // and restores it doesn't need write barriers. It's still // typed as a pointer so that any other writes from Go get // write barriers. sp uintptr pc uintptr g guintptr ctxt unsafe.Pointer ret sys.Uintreg lr uintptr bp uintptr // for GOEXPERIMENT=framepointer }
sp
棧指標位置。pc
程式計數器,執行到的程式位置。
ctxt
- 不常見,可能是一個分配在heap的函式變數,因此GC 需要追蹤它,不過它有可能需要設定並進行清除,在有
寫屏障
- 的時候有些困難。重點了解一下
write barriers
- 。
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種狀態值。許多狀態之間是可以進行改變的。如下圖所示:
type g strcut {
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
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
}
atomicstatus
當前 G 的狀態,上面介紹過 G 的幾種狀態值。syscallsp
如果 G 的狀態為Gsyscall
,那麼值為sched.sp
主要用於GC 期間。syscallpc
如果 G 的狀態為GSyscall
,那麼值為sched.pc
主要用於GC 期間。由此可見這兩個欄位通常一起使用。stktopsp
用於回源跟蹤。param
喚醒 G 時傳入的引數,例如呼叫ready()
。stackLock
棧鎖。
type g struct {
waitsince int64 // approx time when the g become blocked
waitreason waitReason // if status==Gwaiting
}
waitsince
G 阻塞時長。waitreason
阻塞原因。
type g struct {
// asyncSafePoint is set if g is stopped at an asynchronous
// safe point. This means there are frames on the stack
// without precise pointer information.
asyncSafePoint bool
paniconfault bool // panic (instead of crash) on unexpected fault address
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
throwsplit bool // must not split stack
}
asyncSafePoint
非同步安全點;如果 g 在非同步安全點
停止則設定為true
,表示在棧上沒有精確的指標資訊。paniconfault
地址異常引起的 panic(代替了崩潰)。gcscandone
g 掃描完了棧,受狀態_Gscan
位保護。throwsplit
不允許拆分 stack。
type g struct {
// activeStackChans indicates that there are unlocked channels
// pointing into this goroutine's stack. If true, stack
// copying needs to acquire channel locks to protect these
// areas of the stack.
activeStackChans bool
// parkingOnChan indicates that the goroutine is about to
// park on a chansend or chanrecv. Used to signal an unsafe point
// for stack shrinking. It's a boolean value, but is updated atomically.
parkingOnChan uint8
}
activeStackChans
表示是否有未加鎖定的 channel 指向到了 g 棧,如果為 true,那麼對棧的複製需要 channal 鎖來保護這些區域。parkingOnChan
表示 g 是放在 chansend 還是 chanrecv。用於棧的收縮,是一個布林值,但是原子性更新。
type g struct {
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm muintptr
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // pc of go statement that created this goroutine
ancestors *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
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
timer *timer // cached timer for time.Sleep
selectDone uint32 // are we participating in a select and did someone win the race?
}
gopc
建立當前 G 的 pc。startpc
go func 的 pc。timer
通過time.Sleep 快取 timer。
type g struct {
// Per-G GC state
// gcAssistBytes is this G's GC assist credit in terms of
// bytes allocated. If this is positive, then the G has credit
// to allocate gcAssistBytes bytes without assisting. If this
// is negative, then the G must correct this by performing
// scan work. We track this in bytes to make it fast to update
// and check for debt in the malloc hot path. The assist ratio
// determines how this corresponds to scan work debt.
gcAssistBytes int64
}
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
狀態以外的其它狀態組合)
參考資料: