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 都有三個與搶占有關的字段,分別為 preemptpreemptStop 和 premptShrink
  • 每個 G 都有自己的唯一id, 字段為goid,但此字段官方不推薦開發使用。
  • 每個 G 都可以最多綁定一個m,如果可能未綁定,則值為 nil。
  • 每個 G 都有自己內部的 defer 和 panic
  • G 可以被阻塞,並存儲有阻塞原因,字段 waitsince 和 waitreason
  • G 可以被進行 GC 掃描,相關字段為 gcscandoneatomicstatus ( _Gscan 與上面除了_Grunning 狀態以外的其它狀態組合)

參考資料:

  1. go語言教程
  2. go語言深入剖析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM