golang筆記—— Go調(diào)度

一、Runtime

1. 為什么需要runtime

  • goroutines調(diào)度
    goroutines是go的執(zhí)行單元,goroutines如果直接對應(yīng)操作系統(tǒng)的線程,go在調(diào)度goroutines時,勢必將像操作系統(tǒng)調(diào)度線程一樣,需要設(shè)置信號掩碼、CPU親和性及cgroup的資源管理等,這些額外的線程操作不是go運行g(shù)oroutines所需要的,這些操作將消耗大量資源、影響性能。所以go需要runtime來執(zhí)行g(shù)oroutines的調(diào)度,而不是讓操作系統(tǒng)調(diào)度線程。
  • 垃圾回收
    go是需要支持垃圾回收的,執(zhí)行垃圾回收時,我們需要保證goroutines處于暫停的狀態(tài),go的內(nèi)存才會處于一種一致的狀態(tài)。
    當(dāng)沒有調(diào)度器時,線程是由操作系統(tǒng)來調(diào)度,但這對于go而言是不可控的,go無法很好的控制線程狀態(tài)及內(nèi)存。所以為了GC,go需要自己的調(diào)度器。goroutines有g(shù)o自身調(diào)度器控制才能確保內(nèi)存一致,才能正確地執(zhí)行GC。

所以要支持協(xié)程\線程調(diào)度就要有runtime。要支持垃圾回收就要有runtime。

2. 什么是runtime

上面可以分析出runtime所擔(dān)任的職責(zé):goroutines調(diào)度,垃圾回收,當(dāng)然還提供goroutines的執(zhí)行環(huán)境。
所以這也相當(dāng)于簡要解釋了什么是runtime。

go的可執(zhí)行程序可以分成兩個層:用戶代碼和運行時:

  • 運行時提供接口函數(shù)供用戶代碼調(diào)用,用來管理goroutines,channels和其他一些內(nèi)置抽象結(jié)構(gòu)。
  • 用戶代碼對操作系統(tǒng)API的任何調(diào)用都會被運行時層截取,以方便調(diào)度和垃圾回收。


二、GMP調(diào)度模型

  • global queue(全局隊列):存放等待運行的G。為保證數(shù)據(jù)競爭問題,需要加鎖處理。
  • local queue(本地隊列):本地隊列時無鎖的,可以可以提升處理速度。同全局隊列類似,存放的也是等待運行的G,存的數(shù)量有限,不超過256個。新建G'時,G'優(yōu)先加入到P的本地隊列,如果隊列滿了,則會把本地隊列中一半的G移動到全局隊列。
  • P列表:所有的P都在程序啟動時創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS(可配置)個。在任何時候,每個P只能有一個M運行。
  • M:線程想運行任務(wù)就得獲取P,從P的本地隊列獲取G,P隊列為空時,M也會嘗試從全局隊列拿一批G放到P的本地隊列,或從其他P的本地隊列偷一半放到自己P的本地隊列。M運行G,G執(zhí)行之后,M會從P獲取下一個G,不斷重復(fù)下去。

GMP主要包含4個基本單元:g、m、p、schedt。
- g是協(xié)程任務(wù)信息單元
- m是實際執(zhí)行體
- p是本地資源池和本地g任務(wù)池
- schedt是全局資源池和全局g任務(wù)池
下面我們詳細(xì)看下這4個基本單元的主要結(jié)構(gòu):

1. G

G是Goroutine的縮寫,是對Goroutine的抽象。其中包括執(zhí)行的函數(shù)指令及參數(shù);G保存著任務(wù)對象,線程上下文切換,現(xiàn)場保護(hù)和現(xiàn)場恢復(fù)需要的寄存器(SP、IP)等信息。
Goroutine的主要結(jié)構(gòu)(詳見runtime/runtime2.go)

type g struct {
  stack       stack        // 描述了當(dāng)前 Goroutine 的棧內(nèi)存范圍 [stack.lo, stack.hi)
  stackguard0 uintptr // 是對比 Go 棧增長的 prologue 的棧指針, 可以用于調(diào)度器搶占式調(diào)度
  stackguard1 uintptr // 是對比 C 棧增長的 prologue 的棧指針
  ...
  _panic       *_panic // 最內(nèi)側(cè)的 panic 結(jié)構(gòu)體
  _defer       *_defer // 最內(nèi)側(cè)的延遲函數(shù)結(jié)構(gòu)體
  m              *m     // 當(dāng)前的m
  sched          gobuf   // goroutine切換時,用于保存g的上下文      
  ...
  param          unsafe.Pointer // 用于傳遞參數(shù),睡眠時其他goroutine可以設(shè)置param,喚醒時該goroutine可以獲取
  atomicstatus   uint32 // Goroutine 的狀態(tài)
  stackLock      uint32 
  goid           int64  // goroutine的ID
  ...
  waitsince      int64 // g被阻塞的大體時間
  preempt       bool // 搶占信號
  preemptStop   bool // 搶占時將狀態(tài)修改成 `_Gpreempted`
  preemptShrink bool // 在同步安全點收縮棧
  ...
  lockedm        *m     // G被鎖定只在這個m上運行
  ...
}

g中最主要的當(dāng)然是sched了,保存了goroutine的上下文。goroutine切換的時候,不同于線程有OS來負(fù)責(zé)這部分?jǐn)?shù)據(jù),而是由一個gobuf對象來保存,這樣能夠更加輕量級,再來看看gobuf的結(jié)構(gòu)(詳見runtime/runtime2.go):

type gobuf struct {
    // The offsets of sp, pc, and g are known to (hard-coded in) 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 // 程序計數(shù)器
    g    guintptr // 當(dāng)前gobuf所屬的g
    ctxt unsafe.Pointer
    ret  uintptr 系統(tǒng)調(diào)用的返回值
    lr   uintptr
    bp   uintptr // for framepointer-enabled architectures
}

2. M

M是一個線程或稱為Machine,M是有線程棧的。如果不對該線程棧提供內(nèi)存的話,系統(tǒng)會給該線程棧提供內(nèi)存(不同操作系統(tǒng)提供的線程棧大小不同)。當(dāng)指定了線程棧,則M.stack→G.stack,M的PC寄存器指向G提供的函數(shù),然后去執(zhí)行。

type m struct {
    // g0是帶有調(diào)度棧的goroutine。
    // 普通的Goroutine棧是在Heap分配的可增長的stack,而g0的stack是M對應(yīng)的線程棧。
    // 所有調(diào)度相關(guān)代碼,會先切換到該Goroutine的棧再執(zhí)行。
    g0      *g    
    ......
    gsignal       *g         // 處理信號的goroutine
    ......
    tls           [6]uintptr // thread-local storage
    mstartfn      func() //m入口函數(shù)
    curg          *g       // 當(dāng)前運行的goroutine
    caughtsig     guintptr 
    p             puintptr // 關(guān)聯(lián)p和執(zhí)行的go代碼
    nextp         puintptr
    oldp          puintptr // 在執(zhí)行系統(tǒng)調(diào)用之前所附加的p
    id            int32
    mallocing     int32 // 狀態(tài)
    ......
    locks         int32 //m的鎖
    ......
    spinning      bool // m不在執(zhí)行g(shù),但在積極尋找可執(zhí)行的g
    blocked       bool // m是否被阻塞
    newSigstack   bool
    printlock     int8
    incgo         bool // m是否在執(zhí)行cgo
    freeWait      uint32 // 如果為0,將安全釋放g0并刪除m(原子性)。
    fastrand      uint32
    ......
    ncgocall      uint64      // cgo調(diào)用的總數(shù)
    ncgo          int32       // 當(dāng)前cgo調(diào)用的數(shù)目
    ......
    park          note
    alllink       *m // 用于鏈接allm
    schedlink     muintptr
    lockedg       *g // 鎖定g在當(dāng)前m上執(zhí)行,而不會切換到其他m
    createstack   [32]uintptr // thread創(chuàng)建的棧
    ......
    nextwaitm     muintptr    // 下一個等待的m
    ......
}

而g0的棧是M對應(yīng)的線程的棧。所有調(diào)度相關(guān)的代碼,會先切換到該goroutine的棧中再執(zhí)行。也就是說線程的棧也是用的g實現(xiàn),而不是使用的OS的。

3. P

P代表一個處理器,每一個運行的M都必須綁定一個P,就像線程必須在么一個CPU核上執(zhí)行一樣,由P來調(diào)度G在M上的運行,P的個數(shù)就是GOMAXPROCS(最大256),啟動時固定的,一般不修改;M的個數(shù)和P的個數(shù)不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P保存著本地G任務(wù)隊列,也有一個全局G任務(wù)隊列。P的數(shù)據(jù)結(jié)構(gòu):

type p struct {
    id          int32
    status      uint32 // 狀態(tài),可以為pidle/prunning/...
    link        puintptr
    schedtick   uint32     // 每調(diào)度一次加1
    syscalltick uint32     // 每一次系統(tǒng)調(diào)用加1
    sysmontick  sysmontick 
    m           muintptr   // 回鏈到關(guān)聯(lián)的m
    mcache      *mcache //當(dāng)前m的內(nèi)存緩存,意味著不必為每一個M都配備一塊內(nèi)存,避免了過多的內(nèi)存消耗。
    pcache      pageCache
    raceprocctx uintptr
    ......
    // goroutine ids的緩存,攤銷對runtime-sched.goidgen的訪問。
    goidcache    uint64
    goidcacheend uint64
    
    // 可運行的goroutine的隊列. 不需要鎖即可訪問
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr // 下一個運行的g,以高優(yōu)先級執(zhí)行 unblock G,提高了一些包的性能。
    // 可用的G (status == Gdead, Gdead 表示這個goroutine目前未被使用)
    gFree struct {
        gList
        n int32
    }
    // sudog 代表等待列表中的一個G,例如在向通道執(zhí)行發(fā)送/接收的G。
    sudogcache []*sudog
    sudogbuf   [128]*sudog
    // 堆中mspan對象的緩存
    mspancache struct {
        // len 被用于不允許寫障礙的調(diào)用代碼路徑中    
        len int
        buf [128]*mspan
    }
    ......
    palloc persistentAlloc // per-P to avoid mutex
    ......
}

其中P的狀態(tài)有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內(nèi)部隊列runqhead里面有可運行的goroutine,P優(yōu)先從內(nèi)部獲取執(zhí)行的g,這樣能夠提高效率。

4. schedt

除此之外,還有一個數(shù)據(jù)結(jié)構(gòu)需要在這里提及,就是schedt,可以看做是一個全局的調(diào)度者:

type schedt struct {
    // 原子操作訪問
    goidgen   uint64
    lastpoll  uint64 // 最后一次網(wǎng)絡(luò)輪詢的時間,如果正在輪詢則為0
    pollUntil uint64 // 當(dāng)前輪詢的睡眠時間

    lock mutex

    // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
    // sure to call checkdead().

    midle        muintptr // idle狀態(tài)的m
    nmidle       int32    // idle狀態(tài)的m個數(shù)
    nmidlelocked int32    // lockde狀態(tài)的m個數(shù)
    mnext        int64    // 已經(jīng)創(chuàng)建的m的數(shù)量和下一個m的ID
    maxmcount    int32    // m允許的最大個數(shù)
    nmsys        int32    // 非locked狀態(tài)的系統(tǒng)M的數(shù)量
    nmfreed      int64    // 累計可用的M的數(shù)量

    ngsys uint32 // 系統(tǒng)中g(shù)oroutine的數(shù)目,會自動更新

    pidle      puintptr // idle的p
    npidle     uint32
    nmspinning uint32 // 尋找可執(zhí)行g(shù)otoutine的m數(shù)量

    // goroutine全局可運行隊列.
    runq     gQueue
    runqsize int32
    ......
    // _Gdead狀態(tài)G的全局緩存
    gFree struct {
        lock    mutex
        stack   gList // 有stack的Gs
        noStack gList // 沒有stack的Gs
        n       int32
    }

    // sudog的Central 緩存
    sudoglock  mutex
    sudogcache *sudog
    ......
    // 等待被釋放的m的列表
    freem *m
    ......
}

二、GMP調(diào)度細(xì)節(jié)分析

1. schedule

proc.go:3291 findrunnable()


  1. m是否阻塞?阻塞則sleep此m,其他m在從本地隊列尾部獲取g執(zhí)行,執(zhí)行完成后,繼續(xù)執(zhí)行schedule()
  2. 是否需要執(zhí)行g(shù)c?需要gc則停止spining、釋放p、開始sleep,gc完成后會被喚醒,繼續(xù)執(zhí)行schedule()
  3. 若存在gcBgMarkWorker則獲取此g,否則偶爾(1/61概率)會從全局隊列獲取g(避免全局對列饑餓),沒有則從本地隊列獲取g。
  4. 若仍未獲取到g,則會進(jìn)入findrunnable流程,循環(huán)地去找g
  5. 獲取到g后,停止spining
  6. 獲取到鎖定的g,則將把對應(yīng)鎖定的m調(diào)度給當(dāng)前的p并喚醒m。否則該m將移交p給其他等待中的m并喚醒,該m將sleep
  7. 再次進(jìn)入schedule()循環(huán)

2. findrunnable

proc.go:2705 findrunnable():


  1. 是否需要執(zhí)行g(shù)c?需要gc則停止spining、釋放p、開始sleep,gc完成后會被喚醒,繼續(xù)執(zhí)行findrunnable()
  2. 從本地隊列尋找g,沒有則從全局隊列尋找g。獲取g后,將繼續(xù)schedule()
  3. 本地隊列、全局隊列都沒有g(shù),則查看是否存在net或file需要執(zhí)行。存在則獲取g,并繼續(xù)schedule()
  4. 沒有可執(zhí)行的g,開始spining,從其他p的本地隊列尾部竊取一半的g,并繼續(xù)scheudle()
  5. 其他p的本地隊列沒有g(shù),將會在sleep之前,再次嘗試查看全局隊列中是否有g(shù)
  6. 仍沒有g(shù),則釋放p,停止spining并準(zhǔn)備sleep
  7. 檢查是否有idle的p,則sleep,直到再次被喚醒

3. spining

線程自旋(spining)是相對于線程阻塞而言的,表象就是循環(huán)執(zhí)行一個指定邏輯(調(diào)度邏輯,目的是不停地尋找 G)
缺點: 始終獲取不到G時,自旋屬于空轉(zhuǎn),浪費CPU
優(yōu)點: 降低了 M 的上下文切換成本,提高了性能
GMP中有兩個地方會引入自旋:

  • 類型1:沒有P的M找P掛載,保證一有 P 釋放就結(jié)合
  • 類型2:沒有G的M找G運行,保證一有runnable的G就運行

由于P最多只有GOMAXPROCS,所以自旋的M最多只允許GOMAXPROCS個,多了就沒有意義了。
同時當(dāng)有類型1的自旋M存在時,類型2的自旋M就不阻塞,阻塞會釋放P,一釋放P就馬上被類型1的自旋M搶走了,沒必要。

有空閑的 P時,在以下3種場景,go調(diào)度器會確保至少有一個自旋 M 存在(喚醒或者創(chuàng)建一個 M):

  • 新G創(chuàng)建之前
    如果有空閑的 P,就意味著新 G 可以被立即執(zhí)行,即便不在同一個 P 也無妨,所以我們保留一個自旋的 M,就可以保證新 G 很快被運行。
    為了執(zhí)行G,不需要關(guān)注在哪個P上運行,這時應(yīng)該不存在類型 1 的自旋只有類型 2 的自旋
  • M進(jìn)入系統(tǒng)調(diào)用(syscall)之前
    當(dāng) M 進(jìn)入系統(tǒng)調(diào)用,意味著 M 不知道何時可以醒來,那么 M 對應(yīng)的 P 中剩下的 G 就得有新的 M 來執(zhí)行,所以我們保留一個自旋的 M 來執(zhí)行剩下的 G。
    為了執(zhí)行P本地隊列中的G,需要和P綁定,這時應(yīng)該不存在類型 2 的自旋只有類型1的自旋
  • M從空閑變成活躍之前
    如果M從空閑變成活躍,意味著可能一個處于自旋狀態(tài)的M進(jìn)入工作狀態(tài)了,這時要檢查并確保還有一個自旋M存在,以防還有G或者還有P空著的。

4. GMP模式優(yōu)點

  1. G分布在全局隊列和P本地隊列,全局隊列依舊是全局鎖,但是使用場景明顯很少;P 本地隊列使用無鎖隊列,使用原子操作來面對可能的并發(fā)場景。(解決了GM模式單一全局互斥鎖的問題)
  2. G 創(chuàng)建時就在 P 的本地隊列,可以避免在 P 之間傳遞(竊取除外),G 對 P 的數(shù)據(jù)局部性好; 當(dāng) G 開始執(zhí)行了,系統(tǒng)調(diào)用返回后 M 會嘗試獲取可用 P,獲取到了的話可以避免在 M 之間傳遞 。而且優(yōu)先獲取調(diào)用阻塞前的 P,所以 G 對 M 數(shù)據(jù)局部性好,G 對 P 的數(shù)據(jù)局部性也好。(解決了GM模式中G傳遞帶來的開銷問題,以及數(shù)據(jù)局部性問題)
  3. 內(nèi)存 mcache 只存在 P 結(jié)構(gòu)中,就不必為每一個M都配備一塊內(nèi)存了,避免過多的內(nèi)存消耗。P 最多只有 GOMAXPROCS 個,遠(yuǎn)小于 M 的個數(shù),所以也不會出現(xiàn)過多的內(nèi)存消耗。(解決了GM模式中內(nèi)存消耗大的問題)
  4. 通過引入自旋,保證任何時候都有處于等待狀態(tài)的自旋M,避免在等待可用的P和G時頻繁的阻塞和喚醒。(解決了GM模式中嚴(yán)重的線程阻塞/解鎖問題)

5. syscall阻塞情況下的調(diào)度

當(dāng)M1執(zhí)行某一個G時候如果發(fā)生了syscall或者其他阻塞操作后,M1會阻塞。如果當(dāng)前P中仍有一些G待執(zhí)行,runtime會將 Goroutine 的狀態(tài)更新至 _Gsyscall,將 Goroutine 的P和M暫時分離并更新P的狀態(tài)到 _Psyscall,表明這個 P 的 G 正在 syscall 中。

當(dāng)系統(tǒng)調(diào)用結(jié)束后,會調(diào)用退出系統(tǒng)調(diào)用的函數(shù) runtime.exitsyscall 為當(dāng)前 Goroutine 重新分配資源,該函數(shù)有兩個不同的執(zhí)行路徑:

  1. 調(diào)用 runtime.exitsyscallfast;
  2. 切換至調(diào)度器的 Goroutine 并調(diào)用 runtime.exitsyscall0;

采用較快的路徑runtime.exitsyscallfast優(yōu)先來重新獲取原來的P,能獲取到就繼續(xù)綁回去,這樣有利于數(shù)據(jù)的局部性。runtime.exitsyscallfast中包含兩個不同的分支:

  1. 如果 Goroutine 的原P處于 _Psyscall 狀態(tài),會直接調(diào)用 wirep將 Goroutine 與原P進(jìn)行關(guān)聯(lián)
  2. 如果原P不處于 _Psyscall 狀態(tài),且調(diào)度器中存在閑置的P,會調(diào)用 runtime.acquirep 使用閑置的P處理當(dāng)前 Goroutine;

如果通過runtime.exitsyscallfast獲取不到P,runtime就會采用另一個相對較慢的路徑 runtime.exitsyscall0 ,將當(dāng)前 Goroutine 切換至 _Grunnable 狀態(tài),并移除線程 M 和當(dāng)前 Goroutine 的關(guān)聯(lián),然后執(zhí)行以下邏輯分支:

  1. 當(dāng)我們通過 runtime.pidleget 獲取到閑置的處理器時就會在該處理器上執(zhí)行 Goroutine;
  2. 否則找不到空閑的P,runtime就會把 G 放回 global queue,M 放回到 idle list,等待調(diào)度器調(diào)度


6. sysmon搶占調(diào)度

sysmon 也叫監(jiān)控線程,它在一個單獨的 M 上執(zhí)行,無需 P 也可以運行,它是一個死循環(huán),每 20us~10ms 循環(huán)一次,循環(huán)完一次就 sleep 一會。為什么會是一個變動的周期呢,主要是避免空轉(zhuǎn),如果每次循環(huán)都沒什么需要做的事,那么 sleep 的時間就會加大。

1. sysmon的主要作用:

  • 釋放閑置超過 5 分鐘的 span 物理內(nèi)存
  • 如果超過 2 分鐘沒有垃圾回收,強(qiáng)制執(zhí)行
  • 將長時間未處理的 netpoll 添加到全局隊列
  • 向長時間運行的 G 任務(wù)發(fā)出搶占調(diào)度
  • 收回因 syscall 長時間阻塞的 P

2. 滿足什么條件會觸發(fā)搶占調(diào)度呢?

go 1.13 搶占調(diào)度

sysmon發(fā)現(xiàn)一個 P 一直處于running狀態(tài)超過了10ms,將調(diào)用preemptone 將 G 的stackguard0=stackPreempt,同時設(shè)置sched.gcwaiting=1。被標(biāo)記后,在該G調(diào)用新函數(shù)時,通過g.stackguard0判斷是否需要棧增長,需要棧增長就會通過morestack()檢查執(zhí)行schedule(),檢查到sched.gcwaiting==1時,就會讓當(dāng)前G讓出。

G設(shè)置了標(biāo)記位后,也不一定會被搶占。如果G調(diào)用的新函數(shù)是一個簡單的死循環(huán),將無法被搶占。如果G調(diào)用的新函數(shù)所需??臻g很少也不會被搶占,只有當(dāng)新函數(shù)觸發(fā)??臻g檢查(morestack()), 所需棧大于128字節(jié),才會被搶占。那么這里就帶來一些問題,我們看下下面的代碼示例:

package main
import "fmt"

func main(n int) {
    go func(n int){
        for{
            n++
            fmt.Println(n)
        }
    }(0)

    for{}
}

在go 1.13版本之前,執(zhí)行上述代碼,會阻塞在go func()。原因是當(dāng)go的GC觸發(fā)時,會執(zhí)行STW。而STW會搶占所有的P,讓GC來運行。而go func()中是1個簡單的死循環(huán),這類操作無法進(jìn)行newstack、morestack、syscall,所以無法檢測stackguard0 == stackpreempt,也就不會執(zhí)行后續(xù)的schedule()讓當(dāng)前G讓出,導(dǎo)致阻塞。這種依賴棧增長的方式,不算是真正的搶占式調(diào)度。

go 1.14 搶占調(diào)度

在go 1.14版本實現(xiàn)了基于信號的搶占式調(diào)度。

  1. sysmon發(fā)現(xiàn)一個 P 一直處于running狀態(tài)超過了10ms,調(diào)用preemptone()方法時,會通過系統(tǒng)調(diào)用,向m發(fā)送sigPreempt信號。
  2. m收到信號后,會將信號交給sighandler處理
  3. sighandler確定信號為sigPreempt以后,調(diào)用doSigPreempt函數(shù)
  4. doSigPreempt函數(shù)在確認(rèn)P和G允許搶占,并可以安全地執(zhí)行搶占后,會向G的執(zhí)行上下文中注入異步搶占函數(shù)asyncPreempt。
  5. asyncPreempt匯編函數(shù)調(diào)用后,就會保存G的上下文,并調(diào)用schedule()讓當(dāng)前G讓出。

我們可以看到基于信號的搶占式調(diào)度,不再依賴于棧增長,即使空的for{}沒有執(zhí)行棧增長檢測代碼,也依然沒有阻塞,可以成功實現(xiàn)搶占式調(diào)度。

7. netpoller

1. 什么是netpoller
在Go的實現(xiàn)中,期望在用戶層面(程序員層面)所有IO都是阻塞調(diào)用的,Go的設(shè)計思想是程序員使用阻塞式的接口來編寫程序,然后通過goroutine+channel來處理并發(fā)。因此所有的IO邏輯都是直來直去的,先xx,再xx, 你不再需要回調(diào),不再需要future,要的僅僅是step by step。這對于代碼的可讀性是很有幫助的。

但是如果在Runtime內(nèi)部也采用阻塞 I/O 調(diào)用,那么物理線程將也處于阻塞狀態(tài),導(dǎo)致大量資源的浪費。所以Runtime內(nèi)部實際使用的是OS提供的非阻塞IO訪問模式。那么如何將OS的異步I/O與Golang接口的阻塞I/O互相轉(zhuǎn)換呢?golang內(nèi)部就通過OS提供的非阻塞IO訪問模式、并配合epll/kqueue等IO事件監(jiān)控機(jī)制,通過runtime上做的一層封裝,實現(xiàn)將OS的異步I/O與Goroutine的阻塞 I/O互相轉(zhuǎn)換 。這一部分被稱之為netpoller

1. goroutine同步調(diào)用轉(zhuǎn)OS異步調(diào)用
當(dāng)一個goroutine進(jìn)行I/O操作時,并且文件描述符數(shù)據(jù)還沒有準(zhǔn)備好,經(jīng)過一系列的調(diào)用,最后會進(jìn)入gopark函數(shù),gopark將當(dāng)前正在執(zhí)行的goroutine狀態(tài)保存起來,然后切換到新的堆棧上執(zhí)行新的goroutine。由于當(dāng)前goroutine狀態(tài)是被保存起來的,因此后面可以被恢復(fù)。這樣進(jìn)行I/O操作的goroutine以為一直同步阻塞到現(xiàn)在,其實內(nèi)部是異步完成的。


2. goroutine什么時候調(diào)度回來

在schedule()執(zhí)行時,findrunnable()中的netpoll()方法被調(diào)用后,處于就緒狀態(tài)的 fd 對應(yīng)的 G 就會被調(diào)度回來。

8. scheduler affinity

goroutine之間使用channel來回通信時,會導(dǎo)致goroutine頻繁阻塞,導(dǎo)致其在本地隊列會進(jìn)行頻繁地重新排隊,導(dǎo)致goroutine存在被重排后有可能會被竊取的風(fēng)險。


Go 1.5 在 P 中引入了runnext 特殊的一個字段,當(dāng)一組goroutines在communicate-and-wait模式中被阻塞,但很快就runnable了,便會將runnext分別指向這組goroutines。在當(dāng)前G運行結(jié)束,之后將立即執(zhí)行runnext對應(yīng)的G,而不是本地隊列中的G。這允許 goroutine 在再次被阻塞之前能夠快速運行,提高了一部分性能。

六、goroutine的生命周期

- 流程步驟:

  1. runtime創(chuàng)建最初的線程m0和goroutine g0,并把2者關(guān)聯(lián)。
  2. 調(diào)度器初始化:初始化m0、棧、垃圾回收,以及創(chuàng)建和初始化P列表,P的數(shù)目優(yōu)先取環(huán)境變量GOMAXPROCS,否則默認(rèn)是cpu核數(shù)。隨后把第一個P(便于理解可以叫它p0)與m0進(jìn)行綁定,這樣m0就有他自己的p了,就有條件執(zhí)行后續(xù)的任務(wù)g了。
  3. m0的g0會執(zhí)行調(diào)度任務(wù)(runtime.newproc),創(chuàng)建一個g,g指向runtime.main()(還不是我們main包中的main),并放到p的本地隊列。這樣m0就已經(jīng)同時具有了任務(wù)g和p,什么條件都具備了。

runtime.main(): 啟動 sysmon 線程;啟動 GC 協(xié)程;執(zhí)行 init,即代碼中的各種 init 函數(shù);執(zhí)行 main.main 函數(shù)。

  1. 啟動m0,m0已經(jīng)綁定了P,會從P的本地隊列獲取g。
  2. g擁有棧,m根據(jù)g中的棧信息和調(diào)度信息設(shè)置運行環(huán)境
  3. M運行g(shù)
  4. g退出,再次回到M獲取可運行的G,這樣重復(fù)下去,直到main.main退出,runtime.main執(zhí)行Defer和Panic處理,或調(diào)用runtime.exit退出程序。
  • M0
    M0是啟動程序后的編號為0的主線程,這個M對應(yīng)的實例會在全局變量runtime.m0中,不需要在heap上分配,M0負(fù)責(zé)執(zhí)行初始化操作和啟動第一個G, 在之后M0就和其他的M一樣了。
  • G0
    G0是每次啟動一個M都會第一個創(chuàng)建的gourtine,G0僅負(fù)責(zé)調(diào)度,即shedule() 函數(shù), 每個M都會有一個自己的G0。在調(diào)度或系統(tǒng)調(diào)用時會使用G0的棧空間, 全局變量的G0是M0的G0。
  • G0的調(diào)度
    以下3種場景,G0會執(zhí)行調(diào)度:
  1. 當(dāng)前 G 執(zhí)行完成,G0會執(zhí)行調(diào)度獲取下一個G
  2. 當(dāng)前 G 阻塞時:系統(tǒng)調(diào)用、互斥鎖或 chan,G0會執(zhí)行調(diào)度
  3. 在函數(shù)調(diào)用期間,如果當(dāng)前 G 必須擴(kuò)展其堆棧,G0會執(zhí)行調(diào)度

與常規(guī) G 相反,G0 有一個固定和更大的棧。G0除了負(fù)責(zé)G的調(diào)度,還有以下功能:

  • Defer 函數(shù)的分配
  • GC 收集,比如 STW、掃描 G 的堆棧和標(biāo)記、清除操作
  • 棧擴(kuò)容,當(dāng)需要的時候,由 g0 進(jìn)行擴(kuò)棧操作

從 g 到 g0 或從 g0 到 g 的切換是相當(dāng)迅速的,它們只包含少量固定的指令。相反,對于schedule(),執(zhí)行schedule()需要檢查許多資源以便確定下一個要運行的 G。

g0調(diào)度具體流程:

  • 當(dāng)前 g 阻塞在 chan 上并切換到 g0:
    1、g的PC (程序計數(shù)器)和堆棧指針一起保存在內(nèi)部結(jié)構(gòu)中;
    2、將 g0 設(shè)置為正在運行的 goroutine;
    3、g0 的堆棧替換當(dāng)前堆棧;
  • g0 執(zhí)行schedule(),尋找runnable g
  • g0 使用所選的 G 進(jìn)行切換:
    1、PC 和堆棧指針是從G內(nèi)部結(jié)構(gòu)中獲取的;
    2、程序跳轉(zhuǎn)到對應(yīng)的 PC 地址;

References:
http://www.cs.columbia.edu/~aho/cs6998/reports/12-12-11_DeshpandeSponslerWeiss_GO.pdf
https://www.yuque.com/aceld/golang/srxd6d
https://zhuanlan.zhihu.com/p/68299348
https://rakyll.org/scheduler/
https://zhuanlan.zhihu.com/p/27056944
https://www.cnblogs.com/sunsky303/p/11058728.html
https://yizhi.ren/2019/06/03/goscheduler
https://yizhi.ren/2019/06/08/gonetpoller
https://cloud.tencent.com/developer/article/1234360
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容