Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.
??本文基于 Go 1.14。
在 Go 中,協程就是一個包含程序運行時的信息的結構體,如棧,程序計數器,或者它當前的 OS 線程。調度器還必須注意 Goroutine 的開始和退出,這兩個階段需要謹慎管理。
如果你想了解更多關于棧和程序計數器的信息,我推薦你閱讀我的文章 Go:協程切換時涉及到哪些資源?[1]。
開啟
開啟一個協程的處理過程相當簡單。我們用一個程序作為例子:
main 函數在打印信息之前開啟了一個協程。由于協程會有自己的運行時間,因此 Go 通知運行時配置一個新協程,意味著:
- 創建棧
- 收集當前程序計數器或調用方數據的信息
- 更新協程內部數據,如 ID 或 狀態
然而,協程沒有立即獲取運行時狀態。新創建的協程被加入到了本地隊列的最前端,會在 Go 調度的下一周期運行。下面是現在這種狀態的示意圖:
把協程放在隊列的前端,這樣它就會在當前協程運行之后第一個運行。如果有工作竊取發生,它不是在當前線程就是在另一個線程運行。
我推薦你閱讀我的文章 Go: Go 調度器中的工作竊取[2]來獲取更多信息。
在匯編指令中也可以看到協程的創建過程:
協程被創建并被加入到本地協程隊列后,它直接執行主函數的下一個指令。
退出
協程結束時,為了不浪費 CPU 資源,Go 必須調度另一個協程。這也使協程可以在以后復用。
在我的文章 Go: 協程怎么復用?[3]中你可以找到更多信息。
然而,Go 需要一個能識別到協程結束的方法。這個方法是在協程創建時控制的。創建協程時,Go 在將程序計數器設置為協程真實調用的函數之前,將堆棧設置為名為 goexit 的函數。這個技巧可以使協程在結束時必須調 goexit 函數。下面的程序可以使我們理解得更形象:
根據輸出信息進行堆棧追蹤:
/path/to/src/main.go:16
/usr/local/go/src/runtime/asm_amd64.s:1373
用匯編寫的 asm_amd64 文件包含這個函數:
之后,Go 切換到 g0 調度另一個協程。
我們也可以調用 runtime.Goexit() 來手動終止協程:
這個函數首先運行 defer 中的函數,然后會運行前面在協程退出時我們看到的那個函數。
via: https://medium.com/a-journey-with-go/go-how-does-a-goroutine-start-and-exit-2b3303890452
作者:Vincent Blanchon[4]譯者:lxbwolf[5]校對:polaris1119[6]
本文由 GCTT[7] 原創編譯,Go 中文網[8] 榮譽推出
參考資料
[1]
Go:協程切換時涉及到哪些資源?: https://medium.com/a-journey-with-go/go-what-does-a-goroutine-switch-actually-involve-394c202dddb7
[2]
Go: Go 調度器中的工作竊取: https://medium.com/a-journey-with-go/go-work-stealing-in-go-scheduler-d439231be64d
[3]
Go: 協程怎么復用?: https://medium.com/a-journey-with-go/go-how-does-go-recycle-goroutines-f047a79ab352
[4]
Vincent Blanchon: https://medium.com/@blanchon.vincent
[5]
lxbwolf: https://github.com/lxbwolf
[6]
polaris1119: https://github.com/polaris1119
[7]
GCTT: https://github.com/studygolang/GCTT
[8]
Go 中文網: https://studygolang.com/