詳解go語言是如何實現(xiàn)協(xié)程的
寫在文章開頭
go語言
的精華就在于協(xié)程的設(shè)計,只有理解協(xié)程的設(shè)計思想和工作機制,才能確保我們能夠完全的利用協(xié)程編寫強大的并發(fā)程序。
詳解協(xié)程工作機制和實現(xiàn)
協(xié)程示例
正式介紹底層之前,我們給出一段協(xié)程的代碼示例,可以看到筆者開啟一個協(xié)程進行函數(shù)內(nèi)部調(diào)用:
func foo1() { fmt.Println("foo1 調(diào)用 foo2") foo2() } func foo2() { fmt.Println("foo2調(diào)用foo3") foo3() } func foo3() { fmt.Println("foo3 執(zhí)行了") } func main() { //設(shè)置WaitGroup等待協(xié)程結(jié)束 var wg sync.WaitGroup wg.Add(1) go func() { foo1() defer wg.Done() }() //等待上述協(xié)程運行結(jié)束 wg.Wait() }
運行結(jié)果如下:
foo1 調(diào)用 foo2
foo2調(diào)用foo3
foo3 執(zhí)行了
結(jié)合debug我們可以看到當前協(xié)程的調(diào)用棧幀,在函數(shù)調(diào)用前插入一個goexit
的東西,結(jié)合這一點我們開始對協(xié)程的深入剖析:
在這里插入圖片描述
協(xié)程實現(xiàn)結(jié)構(gòu)
在go語言
的協(xié)程結(jié)構(gòu)為:
- 通過一個
stack
記錄其高地址和低地址。 - 通過
sched
的sp(即stackpointer)棧幀的指針
和程序計數(shù)器pc(指向下一條運行的指令)
. - 采用
goid
生成唯一標識。 - 然后再用
atomicstatus
記錄其執(zhí)行狀態(tài)。
基于這幾點我們結(jié)合上述的代碼給出協(xié)程的底層結(jié)構(gòu),如下圖所示,當前協(xié)程的stack
記錄整個foo1函數(shù)的高低地址,假設(shè)我們當前的協(xié)程go
來到foo2
函數(shù)準備調(diào)用foo3
函數(shù),我們的sched
中的sp即stackpointer
記錄foo2的指針,同時因為foo2
內(nèi)部會調(diào)用foo3
所以程序計數(shù)器pc
記錄著調(diào)用foo3
的指令。
最后因為協(xié)程都是由線程調(diào)度的,所以協(xié)程的內(nèi)部也有一個變量記錄著當前線程的指針m:
到此我們了解了協(xié)程核心結(jié)構(gòu),同時我們也在runtime2.go
這一文件中即給出上述所說的核心變量:
type g struct { //記錄棧幀的高地址和低地址 stack stack // offset known to runtime/cgo //...... m *m //執(zhí)行當前協(xié)程的線程指針 //記錄當前堆棧的指針以及下一條指令的運行地址 sched gobuf atomicstatus atomic.Uint32 goid uint64 //...... }
步入stack
可以看到lo
和hi
兩個專門記錄棧幀高低地址的指針:
type stack struct { lo uintptr hi uintptr }
對應(yīng)的我們也給出sched
的類型gobuf
,可以看到sp
和pc
兩個核心指針變量:
type gobuf struct { sp uintptr pc uintptr //...... }
談?wù)刧o語言對于線程的抽象
上文我們提出線程的用m
指針記錄,如下源碼所示,我們都知道在go語言中每個線程都會從一個協(xié)程隊列中獲取協(xié)程執(zhí)行,所以執(zhí)行時它會用curg
記錄當前運行的協(xié)程,然后通過id對自己進行唯一標識,而mOS
則是及記錄當前操作系統(tǒng)信息,這其中最核心的就是g0
它就是每一個線程的操作調(diào)度器:
type m struct { g0 *g // goroutine with scheduling stack id int64 curg *g // current running goroutine mOS }
了解整體結(jié)構(gòu)之后我們再來聊聊go語言線程的g0棧是如何工作的,如下圖所示,每一個g0棧都會通過schedule開始工作:
- 通過execute從協(xié)程隊列中獲取任務(wù)。
- 調(diào)用gogo方法在協(xié)程調(diào)用前插入
go exit
指針它記錄g0棧幀,這個指針就是用于協(xié)程執(zhí)行退出或者掛起是可以通過這個指針跳回g0
棧。 - 然后就是執(zhí)行當前協(xié)程。
- 協(xié)程執(zhí)行完成切換回
g0
棧,重新調(diào)用schedule方法再次從步驟1開始執(zhí)行,由此構(gòu)成一個循環(huán)。
這里我們也給出asm_amd64.s
中關(guān)于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
小結(jié)
自此我們從go語言底層實現(xiàn)的角度完整的剖析的協(xié)程與線程的關(guān)系和實現(xiàn),希望對你有幫助。
以上就是詳解go語言是如何實現(xiàn)協(xié)程的的詳細內(nèi)容,更多關(guān)于go實現(xiàn)協(xié)程的資料請關(guān)注腳本之家其它相關(guān)文章!