前幾天一個小夥伴在公司 slack 問到如下 Golang 代碼爲什麼會卡死(Go Playground):
**package main
import (
"fmt"
"runtime")
func main() {
var i byte
go func() {
for i = 0; i <= 255; i++ {
}
}()
fmt.Println("Dropping mic")
// Yield execution to force executing other goroutines runtime.Gosched()
runtime.GC()
fmt.Println("Done")}**
這個問題很有意思,大概涉及到 Golang 中以下三個概念:
**.byte 是什麼
.goroutine 如何調度
.Golang GC 時會發生什麼**
本文嘗試簡單解釋下爲什麼上面的程序會卡死。
首先,先看下 main 函數裏啓動的 goroutine 事實上是什麼東西:
**var i bytego func() {
for i = 0; i <= 255; i++ {
}}()**
Golang 中,byte 其實被 alias 到 uint8 上了。所以上面的 for 循環會始終成立,因爲 i++ 到
i=255 的時候會溢出,i <= 255 一定成立。也即是, for 循環永遠無法退出,所以上面的代碼其實可以等價於這樣:
**go func() {
for {}}**
其次,Goroutine 的調度是一個非常複雜的問題,這裏並不打算詳細介紹完整細節。
大概描述一下,目前版本的 Golang 中 goroutine 的調度(Scalable Go Scheduler Design Doc)基於 GPM 模型,G 代表 goroutine,M 可以看做真實的資源(OS Threads)。P 是 G-M 的中間層,組織多個 goroutine 跑在同一個 OS Thread 上。大概的模型如下:
圖偷自 Google 圖片搜索
如上圖可以看到,一個 P 上會掛着多個 G,當一個 G 執行結束時,P 會選擇下一個 G 繼續執行。而當一個 G 執行太久沒有結束,總也要給後面的 G 運行的機會吧。所以,Go scheduler 除了在一個 goroutine 執行結束時會調度後面的 goroutine 執行,還會在正在被執行的 goroutine 發生以下情況時讓出當前 goroutine 的執行權,並調度後面的 goroutine 執行:
IO 操作
Channel 阻塞
system call
運行較長時間
前三種這裏我們不關心,最後一種情況下,如果一個 goroutine 執行時間太長,scheduler 會在其 G 對象上打上一個標誌( preempt),當這個 goroutine 內部發生函數調用的時候,會先主動檢查這個標誌,如果爲 true 則會讓出執行權。(這裏說得比較粗略,實際會複雜一些,不過並不是本文重點所以暫不關注細節。)
回到本文開始時的例子,main 函數裏啓動的 goroutine 其實是一個沒有 IO 阻塞、沒有 Channel 阻塞、沒有 system call、沒有函數調用的死循環。也就是,它無法主動讓出自己的執行權,即使已經執行很長時間,scheduler 已經標誌了 preempt。
如上圖所示,一旦這個 G ( goroutine ) 拿到執行權,它後面的 G 將無法再被當前 P 調度獲得執行權。上面程序爲了讓這個 G 對象一定拿到執行權,在 main goroutine 中主動執行 runtime.Gosched() 讓出了執行權。
P 的數量由 GOMAXPROCS 設置,默認爲機器的 CPU 數量。
這裏又分爲兩種情況:
.當這個程序跑在單核機器上的時候,P 默認只有一個,所以一旦調度到這個 G 對象就會卡死,因爲永遠沒有機會再調度回 main goroutine 了。
.當這個程序跑在多核機器上的時候,程序到這一步並不會卡死,因爲另一個 P 所關聯的 G 隊列執行完了之後,會通過 Work-Stealing 算法偷取別的 P 對象上的 G,所以 main goroutine 還是有機會被別的 P 調度到。
可是文章開始時的代碼,不論是在單核的機器上,還是在多核的機器上,都會卡死。
這就涉及到第三個點了:Golang 的 GC。
Golang 的 GC 本質上是基於標記-清除實現的(基於此不斷改進過)。 見名知意,標記-清除分爲兩個階段: - 標記 - 清除
其中,標記階段是需要 STW( Stop The World )的,也就是會讓所有正在運行的 goroutine 停下來。大概源碼在這個位置:
**func gcStart(mode gcMode, trigger gcTrigger) {
// ...... systemstack(stopTheWorldWithSema)
// ......}**
到這一步,死循環這個 goroutine 由於上面介紹的原因永遠無法停下來,但是 main goroutine 阻塞在 GC STW 這裏,等待所有 goroutine 停止執行。main goroutine 在等待一個永遠不會爲它停下的 G,於是,程序卡死了。
類似的,在設置 GOMAXPROCS 的時候,也需要 STW,所以下面的代碼,和本文開始時的代碼,卡死的原因是一樣一樣的(Go Playground):
**package main
import (
"fmt"
"runtime"
"time")
func forever() {
for {
}}
func main() {
go forever()**
**time.Sleep(time.Millisecond) // 讓出執行權 runtime.GOMAXPROCS(1926) // 等待 stw fmt.Println("Done") // 永遠執行不到}**
區區幾行代碼,裏面的奧妙真不少呀。
更多知識
進羣: 313074041
進羣口令(Y1)