曹大帶我學 Go(2)—— 迷惑的 goroutine 執行順序
你好,我是小 X。
曹大最近開 Go 課程了,小 X 正在和曹大學 Go。
這個系列會講一些從課程中學到的讓人醍醐灌頂的東西,撥雲見日,帶你重新認識 Go。
生產端是正在運行的 goroutine 執行 go func(){}()
語句生產出 goroutine 並塞到三級隊列中去。
消費端則是 Go 進程中的 m 在不斷地執行調度循環,從三級隊列中拿到 goroutine 來運行。
生產 - 消費過程
今天我們來通過 2 個實際的代碼例子來看看 goroutine 的執行順序是怎樣的。
第一個例子
首先來看第一個例子:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Println(i)
}()
}
var ch = make(chan int)
<- ch
}
首先通過 runtime.GOMAXPROCS(1)
設置只有一個 P,接着創建了 10 個 goroutine,並分別打印出 i
值。你可以先想一下輸出會是什麼,再對着答案會有更深入的理解。
揭曉答案:
9
0
1
2
3
4
5
6
7
8
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/home/raoquancheng/go/src/hello/main.go:16 +0x96
exit status 2
程序輸出的 fatal error
是因爲 main goroutine 正在從一個 channel 裏讀數據,而這時所有的 channel 都已經掛了,因此出現死鎖。這裏先忽略這個,只需要關注 i
輸出的順序:9, 0, 1, 2, 3, 4, 5, 6, 7, 8
。
我來解釋一下原因:因爲一開始就設置了只有一個 P,所以 for 循環裏面 “生產” 出來的 goroutine 都會進入到 P 的 runnext 和本地隊列,而不會涉及到全局隊列。
每次生產出來的 goroutine 都會第一時間塞到 runnext,而 i 從 1 開始,runnext 已經有 goroutine 在了,所以這時會把 old goroutine 移動 P 的本隊隊列中去,再把 new goroutine 放到 runnext。之後會重複這個過程……
因此這後當一次 i 爲 9 時,新 goroutine 被塞到 runnext,其餘 goroutine 都在本地隊列。
之後,main goroutine 執行了一個讀 channel 的語句,這是一個好的調度時機:main goroutine 掛起,運行 P 的 runnext 和本地可運行隊列裏的 gorotuine。
而我們又知道,runnext 裏的 goroutine 的執行優先級是最高的,因此會先打印出 9,接着再執行本地隊列中的 goroutine 時,按照先進先出的順序打印:0, 1, 2, 3, 4, 5, 6, 7, 8
。
是不是非常有意思?
第二個例子
別急,我們再來看第 2 個例子:
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Println(i)
}()
}
time.Sleep(time.Hour)
}
和第一個例子的不同之處是我們把讀 channel 的代碼換成 Sleep 操作。這一次,你還能正確回答 i
的輸出順序是什麼嗎?
我們直接揭曉答案。
當我們用 go1.13 運行時:
$ go1.13.8 run main.go
0
1
2
3
4
5
6
7
8
而當我們用 go1.14 及之後的版本運行時:
$ go1.14 run main.go
9
0
1
2
3
4
5
6
7
8
可以看到,用 go1.14 及之後的版本運行時,輸出順序和之前的一致。而用 go1.13 運行時,卻先輸出了 0
,這又是什麼原因呢?
這就要從 Go 1.14 修改了 timer 的實現開始說起了。
go 1.13
的 time 包會生產一個名字叫 timerproc 的 goroutine 出來,它專門用於喚醒掛在 timer 上的時間未到期的 goroutine;因此這個 goroutine 會把 runnext 上的 goroutine 擠出去。因此輸出順序就是:0, 1, 2, 3, 4, 5, 6, 7, 8, 9
。
而 go 1.14
把這個喚醒的 goroutine 幹掉了,取而代之的是,在調度循環的各個地方、sysmon 裏都是喚醒 timer 的代碼,timer 的喚醒更及時了,但代碼也更難看懂了。所以,輸出順序和第一個例子是一致的。
總結
今天通過 2 個實際的例子再次複習了 Go 調度消費端的流程,也學到了 time 包在不同 go 版本下的不同之處以及它對程序輸出造成的影響。
有些人還會把例子中的 10 改成比 256 更大的數去嘗試。曹大說這是考眼力,不要給自己找事。因爲這時 P 的本地隊列裝不下這麼多 goroutine 了,只能放到全局隊列。這下程序的輸出順序就不那麼直觀了。
所以,記住本文的核心內容就行了:
-
runnext 的優先級最高。
-
time.Sleep 在老版本中會創建一個 goroutine,在 1.14(包含) 之後不會創建 goroutine 了。
如果被別人考到,知道三級隊列,以及 time 包在 1.14 的變更就行了。
好了,這就是今天全部的內容了~ 我是小 X,我們下期再見~
歡迎關注曹大的 TechPaper 以及碼農桃花源~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/WWfm7Ui7g_gGlb8XkIZigg