go 原始碼分析 goroutine 概覽與排程
goroutine
以後都在 github 更新,請戳 Goroutine–概覽與排程
目錄
相關位置檔案
- src/runtime/runtime2.go
- src/runtime/proc.go
- src/plugin/plugin_dlopen.go
概覽
如果你對 MPG 在Go協程排程中代表什麼並且是如何工作的 感到疑惑, 請先參考 更多資料 中的 scheduling-in-go-part1 到 scheduling-in-go-part3
根據上述文章以及 Go 原始碼中的註釋
M
作業系統執行緒, ‘M’ 是 machine 的簡寫, 這個執行緒其實是被作業系統排程的, 並且作業系統仍然負責把執行緒排程到某個核心上執行
P
邏輯處理器, 執行 Go 程式碼的時候, 必須要獲得的資源. M 必須擁有與之相關聯的 P 才能執行 Go 程式碼. 但是 M 不論在是否存在與之對應的 P 的時候, 都能被阻塞住或者卡在系統呼叫中
G
一個 Goroutine 就是一個 協程, 但是這是 Go, 所以我們把字母 “C”(Coroutine) 替換成 “G”(Goroutine), 你可以把 Goroutine 當成應用級別的執行緒, 他們和系統級別的執行緒在很多地方都非常相似, 只是說作業系統的執行緒是在 CPU 核心上進行上下文切換(切入切出), 而 Goroutine 是在 M 上進行上下文切換(切入切出)
# GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go
# GOSSAFUNC=main go_dev build -gcflags "-S" num_cpu.go
# GODEBUG=schedtrace=DURATION,gctrace=1 go_dev run num_cpu.go
# GODEBUG=schedtrace=DURATION go_dev run num_cpu.go
find . -name '*.go' -exec grep -nHr 'inittask' {} \;
bootstrap 流程的一部分會在預設的 M(M0) 上執行一個 G, 這個 G 會執行 runtime.mstart
, runtime.mstart
最終會進入 schedule
函式, schedule
函式會把 G1 排程到當前的 M(M0) 上執行, G1 會進入 runtime/proc.go
的 main
函式, main
中的 doInit(&runtime_inittask)
會預設初始化並生成至多 N 個 M(系統執行緒), N 是CPU核心數目, 在這之後, G1 執行到函式的末尾並執行 exit(0)
實際上, 在 runtime_inittask
之前, 就會有一個新的 M(thread) 被生成, 一個 goroutine 會在 M 上執行 sysmon
函式, 之後 runtime_inittask
才會生成最多到 N 個 M(執行緒)
我們以更直觀的圖表來展示一下
對於 M1 … MN 中的每個 M, 都會有一個新的 G 在上面執行, 並且每個 M 上都會執行到 runtime.mstart
, 通過 runtime.mstart
最終都會進入 schedule
函式並且讓排程器進行排程
我沒有在原始碼中的 go
或 c
檔案中找到 runtime_inittask
, 但是我在 src/plugin/plugin_dlopen.go
中找到了從 c
動態連結庫中裝載 inittask
的方法, 也許他在 src/runtime/asm_amd64.s
這個彙編程式碼中, 後續找到再來研究這一塊內容(如果你在我之前找到了, 歡迎提 pull request)
排程
可以在 src/runtime/proc.go
找到 schedule
函式的定義
// 執行一輪排程: 找到一個可執行的 goroutine 並執行它
// 這個函式不會返回
func schedule() {
// ...
if gp == nil && gcBlackenEnabled != 0 {
gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
tryWakeP = tryWakeP || gp != nil
}
if gp == nil {
// 隔一段時間檢查一下全域性 goroutine 佇列, 來保證公平
// 不然的話, 兩個在本地佇列的 goroutine 可以通過迴圈互相產生對方作為一個新的 goroutine 的方式, 來讓本地佇列永遠有任務執行
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
gp, inheritTime = runqget(_g_.m.p.ptr())
}
if gp == nil {
gp, inheritTime = findrunnable() // 這個呼叫會阻塞, 知道有任務可以執行為止才返回
}
// ...
execute(gp, inheritTime)
}
排程的流程就比較清晰了
(對於每個P)每隔61次呼叫排程之後, 就會嘗試從全域性佇列裡取一個任務出來, 預設情況則是從本地佇列取一個任務出來(當前P對應的佇列中)
最後一部分的謎團就是執行佇列(run queues), Go 的排程系統中有兩種不同的執行佇列, 全域性執行佇列(Global Run Queue/GRQ)和本地執行佇列(Local Run Queue/LRQ), 每個 P 身上儲存了一個對應的 LRQ 資料結構, 這個結構用來管理並儲存 Goroutine, 這裡的 Goroutine 執行時也需要依賴 P 的環境. 這些 P 身上儲存的 Goroutine 輪流從 M 上面切換上去/切換出來(上下文切換). GRQ 是用來儲存還沒有對應的 P 的 Goroutine 的, 後續會討論一個流程是把 Goroutine 從 GRQ 移動到 LRQ 的 (我: 上面程式碼中已經展示了)
在 schedule
中, findrunnable
會找到一個在當前 P 中可以執行的 goroutine, 本地佇列是儲存在 P 內部的
為什麼
對於作業系統級別的執行緒(M)在 CPU 中進行上下文切換
每一次上下文切換都會導致潛在的約 1000 納秒的延時, 假設硬體的執行速度是每納秒 12 個 CPU 指令, 你會發現線上程切換的時候本來可以執行 12000 個指令, 但是現在並沒有執行, 並且執行緒還可能在 CPU 不同的核心間切換, CPU 快取在這種情況下會失效, 會導致更大的執行緒切換開銷, 更高的延時
對於使用者級別的協程(Goroutinue/G) 在 M 中進行上下文切換, 從作業系統的角度上看, 只是同一個執行緒在不停地執行指令而已, 並且只要你的 Goroutinue 有足夠多的工作要做, 作業系統會認為你是一個 CPU 密集型的執行緒
本質上, Go 把你的 IO/阻塞型 應用在作業系統層面變成了 CPU 密集型應用. 因為所有的上下文切換都發生在了應用層, 當我們通過執行緒執行程式時, 我們不會平均每次上下文切換都丟失大約 12000 個指令的執行時間. 在 Go 裡, 相似的上下文切換佔用大約 200 納秒, 或者 2400 個指令, 並且排程器排程時會考慮到 CPU 快取以及 NUMA. 這也是為什麼我們生成的執行緒數不需大於我們的核心數的原因
在 Go 中, 隨著程式的執行, 我們可以認為更多的任務會被執行並且完成, 因為 Go 排程器會嘗試用盡可能少的執行緒並且在每個執行緒中執行儘可能多的任務(協程), 通過這種方式降低作業系統和硬體的開銷
更多資料
how-a-go-program-compiles-down-to-machine-code
How to call private functions (bind to hidden symbols) in GoLang
相關文章
- goroutine排程原始碼閱讀筆記Go原始碼筆記
- Go語言排程器之排程main goroutine(14)GoAI
- Retrofit原始碼分析一 概覽原始碼
- Linux程式排程邏輯與原始碼分析Linux原始碼
- Go語言goroutine排程器初始化Go
- Go排程器系列(4)原始碼閱讀與探索Go原始碼
- Go語言排程器之盜取goroutine(17)Go
- Go runtime 排程器精講(三):main goroutine 建立GoAI
- Go1.12將支援搶佔式goroutine排程Go
- Golang原始碼學習:排程邏輯(二)main goroutine的建立Golang原始碼AI
- Vuex - 原始碼概覽Vue原始碼
- Redis資料結構概覽(原始碼分析)Redis資料結構原始碼
- 從原始碼分析 GMP 排程原理原始碼
- Go runtime 排程器精講(四):執行 main goroutineGoAI
- 也談goroutine排程器Go
- golang 原始碼分析之scheduler排程器Golang原始碼
- Go runtime 排程器精講(六):非 main goroutine 執行GoAI
- vue-router 原始碼概覽Vue原始碼
- Spring原始碼知識概覽Spring原始碼
- kube-scheduler原始碼分析(3)-搶佔排程分析原始碼
- Java排程執行緒池ScheduledThreadPoolExecutor原始碼分析Java執行緒thread原始碼
- Linux 核心排程器原始碼分析 - 初始化Linux原始碼
- Golang的GMP排程模型與原始碼解析Golang模型原始碼
- libgo原始碼分析之多執行緒協程管理和排程Go原始碼執行緒
- 理解 Go 中的協程(Goroutine)Go
- 詳解Go語言排程迴圈原始碼實現Go原始碼
- 比特幣原始碼分析:任務排程器的使用比特幣原始碼
- 第 12 期 golang 中 goroutine 的排程Golang
- React事件機制 – 原始碼概覽(下)React事件原始碼
- React事件機制 - 原始碼概覽(上)React事件原始碼
- React事件機制 - 原始碼概覽(下)React事件原始碼
- 【Visual Leak Detector】原始碼檔案概覽原始碼
- OpenMP For Construct dynamic 排程方式實現原理和原始碼分析Struct原始碼
- OPENMP FOR CONSTRUCT GUIDED 排程方式實現原理和原始碼分析StructGUIIDE原始碼
- 第三章 Goroutine排程策略(16)Go
- Go runtime 排程器精講(七):案例分析Go
- Go runtime 排程器精講(十一):總覽全域性Go
- [典藏版] Golang 排程器 GMP 原理與排程全分析Golang