Go runtime 可以形象的理解為 Go 程式執行時的環境,類似於 JVM。不同於 JVM 的是,Go 的 runtime 與業務程式直接打包在一塊,是一個可執行檔案,直接執行在作業系統上,效率很高。
runtime 包含了一些 Go 的一些非常核心的功能:協程排程、垃圾回收、記憶體分配等。本文將著重介紹協程排程(GMP 模型)。
協程排程是指 Go 如何管理和執行協程,Go 的協程排程基於 GMP 模型。即:
GMP 三者的關係:
假如主機是單邏輯 CPU 的,那麼 GMP 是這樣的:
紅色部分表示休眠或者掛起狀態,黃色代表等待執行,綠色表示正在執行。系統初始化了兩個執行緒,但我們只有一個處理器(P), M1 沒有獲取到 P,所以只能休眠。M0 當前獲取到 P ,正在處理 G0, LRQ 裡面目前有三個 G 在排隊等待被 M 執行,GRQ 裡面儲存著 G4、G5、G6,表示它們還沒有分配到佇列中。
P 這個時候會分別對 LRQ 進行週期佇列輪轉 和 GRO 週期性檢查:
假設 G0 遇到了系統呼叫:
等到 M1 中所有的協程執行完或者 M1 處理某個協程也遇到了了系統呼叫,就會重新釋放 P 給其他空閒的 M。而另外一邊 G0 的系統呼叫結束後,就會將 M0 執行緒從掛起狀態變成休眠狀態,並將 G0 放入 GRQ,等待被 P 重新調入 LRQ 中輪轉執行。
如果我們的主機具備多個邏輯 CPU,建立了多個 P,那麼就會變成多個執行緒並行執行:
多執行緒同時處理時,很有可能多個 LRQ 是不均衡的。假如上圖的 M0 已經執行完了,其他執行緒還處於繁忙狀態,M0 所繫結的 P 就會去檢查 GQR,GQR 中也沒有 G,那麼它就會去偷取其他 LRQ 一部分的 G 來執行,一般每次會偷取一半。
runtime.GOMAXPROCS()
可以用來設定 P 的數量,一般設定為和邏輯 CPU 數量相等的值:
fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(runtime.NumCPU()) // 使用所有的邏輯 CPU
// 結果
我的主機 CPU 是16核24執行緒,所以會使用24個 P
runtime.Gosched()
用於讓出當前協程的執行時間片,也就是當 P 遇到它時,會先安排其他協程先執行:
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 5; i++ {
fmt.Println("go")
}
}()
runtime.Gosched()
fmt.Println("hello")
}
// 結果
輸入結果不是固定的,有可能是
go
go
go
go
go
hello
也有可能是
go
go
go
go
hello
go
也有可能是
hello
輸出第一種情況容易理解,主協程讓出了時間片,理所應當先列印 Go,但是如果子協程還沒有來得及被排程或者列印,就會出現其他情況。
runtime.Goexit() 會結束當前的協程,但是 defer 語句會正常執行。此語法不能在主函數中使用,會引發 panic:
func main() {
runtime.GOMAXPROCS(1)
go func() {
defer fmt.Println("defer不受影響")
fmt.Println("我被執行了")
runtime.Goexit()
fmt.Println("我被跳過了")
}()
time.Sleep(1 * time.Second)
}
// 結果
我被執行了
defer不受影響
本系列文章: