This page looks best with JavaScript enabled

Scheduling in Go Part II

 ·  ☕ 3 min read

https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html
以下截圖都是從這個網站來的

Part II: Go Scheduler

這部分要開始介紹 Go 的 Scheduler 是怎麼運作的,以及它的優勢在哪裡

在 Go 裡面執行 runtime.NumCPU() 可以知道目前的電腦有幾個 virtual core (看有幾個cpu、各有幾個核心、核心上有幾個hardware thread),這也是你的電腦最多可以平行執行的任務數量 (Part I 提到過)。

用 runtime.GOMAXPROCS(n) 可以設定 Go 給予 n 個 ‘P’, logical processor,設定 Go 可以’邏輯上’平行執行的任務數量 (也就是要有幾個 active thread)。
當 P 的數量大於前面 runtime.NumCPU() 數量,這些 P 就可能要常常交換使用 core 造成 context switch 負擔。

如Part I所說,如果執行中的 thread 太少就無法利用全部的 core,太多的話這些 thread 會需要做 context switch 減慢速度。所以 Go 就把預設值設為 runtime.NumCPU()的數量。
但在很少數的情況下 GOMAXPROCS 設高反而可以增加一點效能,參考 https://colobu.com/2017/10/11/interesting-things-about-GOMAXPROCS/

Go Scheduler 是使用一個叫做 GMP model 的東西包含以下三個元件
G: Goroutine, 一些要做的任務
M: Machine thread, 就是 OS 層級的 thread
P: Logical processor, 可以把他想成任務執行器,管理自己的 queue (Local run queue),裡面有很多 G 待做

除此之外還有一個 Global run queue 存放孤兒 Goroutine

要執行任務計算(G)時,P 必須與一個 M 接在一起,並使用一個 core 來執行如下圖 (夾在 M 和 P 中間的就是執行中的 G)

Goroutine states, context switch

與 thread 相同,也有 等待(例如等IO)、可執行、執行中三個狀態

執行中代表上圖中 G 在 M 與 P 之間
可執行的 G 就會在 local run queue 或 global run queue 排隊等著被執行
等待狀態的 G 需要等待某些資源才能繼續被執行,可能會在 Network poller 中等待非同步的動作,或著與 M 一起被 block 住後兩個一起去旁邊等待, 下面的 3. 4點就會出現這個狀態的 G

Goroutine 的 context switch 將 G 從執行中換下來, 把其他 G 換上來執行,但這只發生在必要的時刻,經常做只會多花時間
可能發生的情況有幾種

  1. 產生新的 goroutine
    go func()… 有可能會c ontext switch,但不一定

  2. Garbage collection
    GC 有自己的 Goroutine 要做也需要 M, 所以要把其他G換下來來執行收垃圾動作

  3. Synchronization and Orchestration
    例如等待 channel 的資料被 block 的時候, 或者是 mutext 會把 goroutine 卡住的動作都可能會有 context switch

  4. system call
    分為 asynchronous 與 synchronous system call

    非同步的狀況,例如大部分 OS 中 Networking-based system calls,這種情況 G 可以從 M 和 P 上拔下來,去 Network poller 中等待結果 (依靠 OS 的非同步 IO 達成,例如 epoll),過程可以參考網頁內 Figure 3,4,5 ,G 得到結果之後再把 G 塞回 queue 中等待執行。期間 M 與 P 可以繼續消化其他 G。

    同步的 system call 例如某些 File-based system calls,會把整個 M block 住 (OS 無法非同步的處理這類型的 system call),這時做這個呼叫的 G 會和 M 一起被放去旁邊等待結果 (thread M 會被從 core 上 context switch 下來,效率比前一種差),不佔用 cpu 資源,流程參考網頁內 Figure 6,7,8,而 P 會取得一條新的 M (找 idle 的或新創一條) 來執行其他 G。當旁邊等待的 GM 得到結果後 G 會被塞回原本的queue中, M 則是變成idel等其他人用,需要時就不必新建 (正確來說是變成 spinning thread,主動去找孤兒P配對,也會使用到 cpu 資源,但通常比起刪掉重創來的划算,參考 https://zhuanlan.zhihu.com/p/42057783)。

Work Stealing

簡單來說就是 P 管理的 queue 中的 G 可以在不同 P 的 queue 之間移動

當一個 P 發現自己沒有 G 可以做了,有可能會去 Global run queue 撿工作,也可能去別的 P 搶工作來做。

當然這也要花時間,太過頻繁的搬動 G 也不好

最後作者給了一個範例
總體來說 goroutine 切換和 thread 之間的切換很類似,但 goroutine 的 size 小,所以可以創造很多個,context switch 速度也快很多,相比以往每個任務都開啟一條 thread,也省去了許多 thread 的產生銷毀耗費,所以速度與資源使用才會比較好

下面這張圖是一個韓國人的投影片其中的一頁
https://www.slideshare.net/Hyejong/golang-restful

總結了 Goroutine 與 thread 資源的差距

PS. context switch 時間約是 200ns 與 1000ns,5倍的差距

Share on

Marko Peng
WRITTEN BY
Marko Peng
Good man