go 語言是如何實現協程的
go語言
的精華就在於協程的設計,只有理解協程的設計思想和工作機制,才能確保我們能夠完全的利用協程編寫強大的併發程序。
Hi,我是 sharkChili ,是個不斷在硬核技術上作死的 java coder ,是 CSDN 的博客專家 ,也是開源項目 Java Guide 的維護者之一,熟悉 Java 也會一點 Go ,偶爾也會在 C 源碼 邊緣徘徊。寫過很多有意思的技術博客,也還在研究並輸出技術的路上,希望我的文章對你有幫助,非常歡迎你關注我的公衆號: 寫代碼的 SharkChili 。
詳解協程工作機制和實現
協程示例
正式介紹底層之前,我們給出一段協程的代碼示例,可以看到筆者開啓一個協程進行函數內部調用:
func foo1() {
fmt.Println("foo1 調用 foo2")
foo2()
}
func foo2() {
fmt.Println("foo2調用foo3")
foo3()
}
func foo3() {
fmt.Println("foo3 執行了")
}
func main() {
//設置WaitGroup等待協程結束
var wg sync.WaitGroup
wg.Add(1)
go func() {
foo1()
defer wg.Done()
}()
//等待上述協程運行結束
wg.Wait()
}
運行結果如下:
foo1 調用 foo2
foo2調用foo3
foo3 執行了
結合 debug 我們可以看到當前協程的調用棧幀,在函數調用前插入一個goexit
的東西,結合這一點我們開始對協程的深入剖析:
協程實現結構
在go語言
的協程結構爲:
-
通過一個
stack
記錄其高地址和低地址。 -
通過
sched
的sp(即stackpointer)棧幀的指針
和程序計數器pc(指向下一條運行的指令)
. -
採用
goid
生成唯一標識。 -
然後再用
atomicstatus
記錄其執行狀態。
基於這幾點我們結合上述的代碼給出協程的底層結構,如下圖所示,當前協程的stack
記錄整個 foo1 函數的高低地址,假設我們當前的協程go
來到foo2
函數準備調用foo3
函數,我們的sched
中的 sp 即stackpointer
記錄 foo2 的指針,同時因爲foo2
內部會調用foo3
所以程序計數器pc
記錄着調用foo3
的指令。
最後因爲協程都是由線程調度的,所以協程的內部也有一個變量記錄着當前線程的指針 m:
到此我們瞭解了協程核心結構,同時我們也在runtime2.go
這一文件中即給出上述所說的核心變量:
type g struct {
//記錄棧幀的高地址和低地址
stack stack // offset known to runtime/cgo
//......
m *m //執行當前協程的線程指針
//記錄當前堆棧的指針以及下一條指令的運行地址
sched gobuf
atomicstatus atomic.Uint32
goid uint64
//......
}
步入stack
可以看到lo
和hi
兩個專門記錄棧幀高低地址的指針:
type stack struct {
lo uintptr
hi uintptr
}
對應的我們也給出sched
的類型gobuf
,可以看到sp
和pc
兩個核心指針變量:
type gobuf struct {
sp uintptr
pc uintptr
//......
}
談談 go 語言對於線程的抽象
上文我們提出線程的用m
指針記錄,如下源碼所示,我們都知道在 go 語言中每個線程都會從一個協程隊列中獲取協程執行,所以執行時它會用curg
記錄當前運行的協程,然後通過 id 對自己進行唯一標識,而mOS
則是及記錄當前操作系統信息,這其中最核心的就是g0
它就是每一個線程的操作調度器:
type m struct {
g0 *g // goroutine with scheduling stack
id int64
curg *g // current running goroutine
mOS
}
瞭解整體結構之後我們再來聊聊 go 語言線程的 g0 棧是如何工作的,如下圖所示,每一個 g0 棧都會通過 schedule 開始工作:
-
通過 execute 從協程隊列中獲取任務。
-
調用 gogo 方法在協程調用前插入
go exit
指針它記錄 g0 棧幀,這個指針就是用於協程執行退出或者掛起是可以通過這個指針跳回g0
棧。 -
然後就是執行當前協程。
-
協程執行完成切換回
g0
棧,重新調用 schedule 方法再次從步驟 1 開始執行,由此構成一個循環。
這裏我們也給出asm_amd64.s
中關於gogo
的彙編代碼,可以看到 gobuf_sp 方法它會記錄當前stack pointer
也就是我們上文針對g0
所說的g0
棧地址:
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
MOVQ DX, R14 // set the g register
//記錄g0棧地址
MOVQ gobuf_sp(BX), SP // restore SP
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX
JMP BX
小結
自此我們從 go 語言底層實現的角度完整的剖析的協程與線程的關係和實現,希望對你有幫助。
我是 sharkchili ,CSDN Java 領域博客專家,開源項目—JavaGuide contributor,我想寫一些有意思的東西,希望對你有幫助,如果你想實時收到我寫的硬核的文章也歡迎你關注我的公衆號: 寫代碼的 SharkChili 。 因爲近期收到很多讀者的私信,所以也專門創建了一個交流羣,感興趣的讀者可以通過上方的公衆號獲取筆者的聯繫方式完成好友添加,點擊備註 “加羣” 即可和筆者和筆者的朋友們進行深入交流。
參考
程序計數器(PC)、堆棧指針(SP)與函數調用過程: https://www.cnblogs.com/uestcliming666/p/11488782.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ghDuP0PEaUworvl0Z6IhwA