原創文章,歡迎轉載,轉載請註明出處,謝謝。
0. 前言
在 Go runtime 排程器精講(三):main goroutine 建立 介紹了 main goroutine 的建立,文中我們說 main goroutine 和非 main goroutine 有區別。當時賣了個關子並未往下講,這一講我們會繼續介紹非 main goroutine (也就是 go 關鍵字建立的 goroutine,後文統稱為 gp) 的執行,並且把這個關子解開,說一說它們的區別在哪兒。
1. gp 的建立
首先看一個示例:
func g2() {
time.Sleep(10 * time.Second)
println("hello world")
}
func main() {
go g2()
time.Sleep(1 * time.Minute)
println("main exit")
}
main 函式建立兩個 goroutine,一個 main goroutine,一個普通 goroutine。從 Go runtime 排程器精講(四):執行 main goroutine 可知 main goroutine 執行完之後就呼叫 exit(0)
退出了。為了能進入 gp,我們這裡在 main goroutine 中加了 1 分鐘的等待時間。
Go runtime 的啟動在前幾講都有介紹,這裡直接進入 main 函式,檢視 gp 是如何建立的:
(dlv) c
> main.main() ./goexit.go:12 (hits goroutine(1):1 total:1) (PC: 0x46238a)
7: func g2() {
8: time.Sleep(10 * time.Second)
9: println("hello world")
10: }
11:
=> 12: func main() {
13: go g2()
14:
15: time.Sleep(30 * time.Minute)
16: println("main exit")
17: }
直接看 main 函式,我們看不出 go
關鍵字做了什麼,檢視 CPU 的彙編指令:
(dlv) si
> main.main() ./goexit.go:13 (PC: 0x462395)
goexit.go:12 0x462384 7645 jbe 0x4623cb
goexit.go:12 0x462386 55 push rbp
goexit.go:12 0x462387 4889e5 mov rbp, rsp
goexit.go:12 0x46238a* 4883ec10 sub rsp, 0x10
goexit.go:13 0x46238e 488d050b7a0100 lea rax, ptr [rip+0x17a0b]
=> goexit.go:13 0x462395 e8c6b1fdff call $runtime.newproc
goexit.go:15 0x46239a 48b800505c18a3010000 mov rax, 0x1a3185c5000
goexit.go:15 0x4623a4 e8b79fffff call $time.Sleep
可以看到,go
關鍵字被編譯轉換後實際呼叫的是 $runtime.newproc
函式,這個函式在 Go runtime 排程器精講(四):執行 main goroutine 已經非常詳細的介紹過了,這裡就不贅述了。
有必要在說明的是,main goroutine 和普通 goroutine 執行的順序。當呼叫 runtime.newproc
後,gp 被新增到 P 的可執行佇列(如果佇列滿,被新增到全域性佇列),接著執行緒會排程執行該 gp。不過對於 newproc
來說,gp 放入佇列後,newproc
就退出了。接著執行後續的 main goroutine 程式碼。
如果此時 gp 未執行或者未結束,並且 main goroutine 未等待/阻塞的話,main goroutine 將直接退出。
2. gp 的退出
前面說 gp 和 main goroutine 的區別主要體現在 goroutine 的退出這裡。main goroutine 的退出比較殘暴,直接呼叫 exit(0)
退出程序。那麼,gp 是怎麼退出的呢?
我們在 g2
結束點處打斷點,看看 g2
是怎麼退出的:
(dlv) b ./goexit.go:10
Breakpoint 1 set at 0x46235b for main.g2() ./goexit.go:10
(dlv) c
hello world
> main.g2() ./goexit.go:10 (hits goroutine(5):1 total:1) (PC: 0x46235b)
7: func g2() {
8: time.Sleep(10 * time.Second)
9: println("hello world")
=> 10: }
11:
12: func main() {
13: go g2()
14:
15: time.Sleep(30 * time.Minute)
(dlv) si
> main.g2() ./goexit.go:10 (PC: 0x46235f)
goexit.go:9 0x462345 488d05b81b0100 lea rax, ptr [rip+0x11bb8]
goexit.go:9 0x46234c bb0c000000 mov ebx, 0xc
goexit.go:9 0x462351 e88a30fdff call $runtime.printstring
goexit.go:9 0x462356 e86528fdff call $runtime.printunlock
goexit.go:10 0x46235b* 4883c410 add rsp, 0x10
=> goexit.go:10 0x46235f 5d pop rbp
goexit.go:10 0x462360 c3 ret
goexit.go:7 0x462361 e89ab1ffff call $runtime.morestack_noctxt
goexit.go:7 0x462366 ebb8 jmp $main.g2
CPU 執行指令到 pop rbp
,接著執行 ret:
goexit.go:10 0x46235f 5d pop rbp
=> goexit.go:10 0x462360 c3 ret
goexit.go:7 0x462361 e89ab1ffff call $runtime.morestack_noctxt
goexit.go:7 0x462366 ebb8 jmp $main.g2
(dlv) si
> runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1651 (PC: 0x45d7a1)
Warning: debugging optimized function
TEXT runtime.goexit(SB) /usr/local/go/src/runtime/asm_amd64.s
asm_amd64.s:1650 0x45d7a0 90 nop
=> asm_amd64.s:1651 0x45d7a1 e8ba250000 call $runtime.goexit1
asm_amd64.s:1653 0x45d7a6 90 nop
我們看到了什麼,執行 ret 直接跳轉到了 call $runtime.goexit1
。還記得在 Go runtime 排程器精講(三):main goroutine 建立 中說每個 goroutine 棧都會在“棧頂”放 funcPC(goexit) + 1
的地址。這裡實際是做了一個偷樑換柱,gp 的棧在退出執行 ret 時都會跳轉到 call $runtime.goexit1
繼續執行。
進入 runtime.goexit1
:
// Finishes execution of the current goroutine.
func goexit1() {
...
mcall(goexit0) // mcall 會切換當前棧到 g0 棧,接著在 g0 棧執行 goexit0
}
實際執行的是 goexit0
:
// goexit continuation on g0.
func goexit0(gp *g) {
mp := getg().m // 這裡是 g0 棧,mp = m0
pp := mp.p.ptr() // m0 繫結的 P
casgstatus(gp, _Grunning, _Gdead) // 將 gp 的狀態更新為 _Gdead
gp.m = nil // 將 gp 繫結的執行緒更新為 nil,和執行緒解綁
...
dropg() // 將當前執行緒和 gp 解綁
...
gfput(pp, gp) // 退出的 gp 還是可以重用的,gfput 將 gp 放到本地或者全域性空閒佇列中
...
schedule() // 執行緒執行完一個 gp 還沒有退出,繼續進入 schedule 找 goroutine 執行
}
gp 退出了,執行緒並沒有退出,執行緒將 gp 安頓好之後,繼續開始新一輪排程,真是勞模啊。
3. 小結
本講介紹了用 go
關鍵字建立的 goroutine 是如何執行的,下一講我們放鬆放鬆,看幾個案例分析排程器的行為。