golang筆記—— Go調度

一、Runtime

1. 為什么需要runtime

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

所以要支持協程\線程調度就要有runtime。要支持垃圾回收就要有runtime。

2. 什么是runtime

上面可以分析出runtime所擔任的職責:goroutines調度,垃圾回收,當然還提供goroutines的執行環境。
所以這也相當于簡要解釋了什么是runtime。

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

  • 運行時提供接口函數供用戶代碼調用,用來管理goroutines,channels和其他一些內置抽象結構。
  • 用戶代碼對操作系統API的任何調用都會被運行時層截取,以方便調度和垃圾回收。


二、GMP調度模型

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

GMP主要包含4個基本單元:g、m、p、schedt。
- g是協程任務信息單元
- m是實際執行體
- p是本地資源池和本地g任務池
- schedt是全局資源池和全局g任務池
下面我們詳細看下這4個基本單元的主要結構:

1. G

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

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

g中最主要的當然是sched了,保存了goroutine的上下文。goroutine切換的時候,不同于線程有OS來負責這部分數據,而是由一個gobuf對象來保存,這樣能夠更加輕量級,再來看看gobuf的結構(詳見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 // 程序計數器
    g    guintptr // 當前gobuf所屬的g
    ctxt unsafe.Pointer
    ret  uintptr 系統調用的返回值
    lr   uintptr
    bp   uintptr // for framepointer-enabled architectures
}

2. M

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

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

而g0的棧是M對應的線程的棧。所有調度相關的代碼,會先切換到該goroutine的棧中再執行。也就是說線程的棧也是用的g實現,而不是使用的OS的。

3. P

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

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

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

其中P的狀態有Pidle, Prunning, Psyscall, Pgcstop, Pdead;在其內部隊列runqhead里面有可運行的goroutine,P優先從內部獲取執行的g,這樣能夠提高效率。

4. schedt

除此之外,還有一個數據結構需要在這里提及,就是schedt,可以看做是一個全局的調度者:

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

    lock mutex

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

    midle        muintptr // idle狀態的m
    nmidle       int32    // idle狀態的m個數
    nmidlelocked int32    // lockde狀態的m個數
    mnext        int64    // 已經創建的m的數量和下一個m的ID
    maxmcount    int32    // m允許的最大個數
    nmsys        int32    // 非locked狀態的系統M的數量
    nmfreed      int64    // 累計可用的M的數量

    ngsys uint32 // 系統中goroutine的數目,會自動更新

    pidle      puintptr // idle的p
    npidle     uint32
    nmspinning uint32 // 尋找可執行gotoutine的m數量

    // goroutine全局可運行隊列.
    runq     gQueue
    runqsize int32
    ......
    // _Gdead狀態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調度細節分析

1. schedule

proc.go:3291 findrunnable()


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

2. findrunnable

proc.go:2705 findrunnable():


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

3. spining

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

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

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

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

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

4. GMP模式優點

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

5. syscall阻塞情況下的調度

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

當系統調用結束后,會調用退出系統調用的函數 runtime.exitsyscall 為當前 Goroutine 重新分配資源,該函數有兩個不同的執行路徑:

  1. 調用 runtime.exitsyscallfast
  2. 切換至調度器的 Goroutine 并調用 runtime.exitsyscall0

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

  1. 如果 Goroutine 的原P處于 _Psyscall 狀態,會直接調用 wirep將 Goroutine 與原P進行關聯
  2. 如果原P不處于 _Psyscall 狀態,且調度器中存在閑置的P,會調用 runtime.acquirep 使用閑置的P處理當前 Goroutine;

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

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


6. sysmon搶占調度

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

1. sysmon的主要作用:

  • 釋放閑置超過 5 分鐘的 span 物理內存
  • 如果超過 2 分鐘沒有垃圾回收,強制執行
  • 將長時間未處理的 netpoll 添加到全局隊列
  • 向長時間運行的 G 任務發出搶占調度
  • 收回因 syscall 長時間阻塞的 P

2. 滿足什么條件會觸發搶占調度呢?

go 1.13 搶占調度

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

G設置了標記位后,也不一定會被搶占。如果G調用的新函數是一個簡單的死循環,將無法被搶占。如果G調用的新函數所需棧空間很少也不會被搶占,只有當新函數觸發棧空間檢查(morestack()), 所需棧大于128字節,才會被搶占。那么這里就帶來一些問題,我們看下下面的代碼示例:

package main
import "fmt"

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

    for{}
}

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

go 1.14 搶占調度

在go 1.14版本實現了基于信號的搶占式調度

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

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

7. netpoller

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

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

1. goroutine同步調用轉OS異步調用
當一個goroutine進行I/O操作時,并且文件描述符數據還沒有準備好,經過一系列的調用,最后會進入gopark函數,gopark將當前正在執行的goroutine狀態保存起來,然后切換到新的堆棧上執行新的goroutine。由于當前goroutine狀態是被保存起來的,因此后面可以被恢復。這樣進行I/O操作的goroutine以為一直同步阻塞到現在,其實內部是異步完成的。


2. goroutine什么時候調度回來

在schedule()執行時,findrunnable()中的netpoll()方法被調用后,處于就緒狀態的 fd 對應的 G 就會被調度回來。

8. scheduler affinity

goroutine之間使用channel來回通信時,會導致goroutine頻繁阻塞,導致其在本地隊列會進行頻繁地重新排隊,導致goroutine存在被重排后有可能會被竊取的風險。


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

六、goroutine的生命周期

- 流程步驟:

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

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

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

與常規 G 相反,G0 有一個固定和更大的棧。G0除了負責G的調度,還有以下功能:

  • Defer 函數的分配
  • GC 收集,比如 STW、掃描 G 的堆棧和標記、清除操作
  • 棧擴容,當需要的時候,由 g0 進行擴棧操作

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

g0調度具體流程:

  • 當前 g 阻塞在 chan 上并切換到 g0:
    1、g的PC (程序計數器)和堆棧指針一起保存在內部結構中;
    2、將 g0 設置為正在運行的 goroutine;
    3、g0 的堆棧替換當前堆棧;
  • g0 執行schedule(),尋找runnable g
  • g0 使用所選的 G 進行切換:
    1、PC 和堆棧指針是從G內部結構中獲取的;
    2、程序跳轉到對應的 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375