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 換上來執行,但這只發生在必要的時刻,經常做只會多花時間
可能發生的情況有幾種
-
產生新的 goroutine
go func()… 有可能會c ontext switch,但不一定 -
Garbage collection
GC 有自己的 Goroutine 要做也需要 M, 所以要把其他G換下來來執行收垃圾動作 -
Synchronization and Orchestration
例如等待 channel 的資料被 block 的時候, 或者是 mutext 會把 goroutine 卡住的動作都可能會有 context switch -
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倍的差距