這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
概述
協程是Golang中的輕量級線程,麻雀雖小五髒俱全,Golang管理協程時也必然會涉及到協程之間的切換:阻塞的協程被切換出去,可啟動並執行協程被切換進來。我們在本章節就來仔細分析下協程如何切換。
TLS
thread local storage:
getg()
goget()用來擷取當前線程正在執行的協程g。該協程g被儲存在TLS中。
mcall()
mcall在golang需要進行協程切換時被調用,用來儲存被切換出去協程的資訊,並在當前線程的g0協程堆棧上執行新的函數。一般情況下,會在新函數中執行一次schedule()來挑選新的協程來運行。接下來我們就看看mcall的實現。
調用時機
系統調用返回
當執行系統調用的線程從系統調用中返回後,有可能需要執行一次新的schedule,此時可能會調用mcall來完成該工作,如下:
func exitsyscall(dummy int32) { ...... // Call the scheduler. mcall(exitsyscall0) ......}
在exitsyscall0中如果可能會放棄當前協程並執行一次schedule,挑選新的協程來佔有m。
由於阻塞放棄執行
由於某些原因,當前執行的協程可能會被阻塞,如管道讀寫時條件無法滿足,則當前協程會被阻塞直到條件滿足。
在gopark()函數中,便會調用該mcall放棄當前協程並執行一次協程調度。
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) { mp := acquirem() gp := mp.curg status := readgstatus(gp) if status != _Grunning && status != _Gscanrunning { throw("gopark: bad g status") } mp.waitlock = lock mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf)) gp.waitreason = reason mp.waittraceev = traceEv mp.waittraceskip = traceskip releasem(mp) // can't do anything that might move the G between Ms here. mcall(park_m)}
而park_m函數我們在後面會分析,它放棄之前執行的協程並調用一次schedule()挑選新的協程來執行。
執行原理
前面我們主要描述了mcall被調用的時機,現在我們要來看看mcall的實現原理。
mcall的函數原型是:
func mcall(fn func(*g))
這裡fn的參數指的是在調用mcall之前正在啟動並執行協程。
我們前面說到,mcall的主要作用是協程切換,它將當前正在執行的協程狀態儲存起來,然後在m->g0的堆棧上調用新的函數。 在新的函數內會將之前啟動並執行協程放棄,然後調用一次schedule()來挑選新的協程運行。
// func mcall(fn func(*g)) // Switch to m->g0's stack, call fn(g). // Fn must never return. It should gogo(&g->sched) // to keep running g. TEXT runtime·mcall(SB), NOSPLIT, $0-8 // DI中儲存參數fn MOVQ fn+0(FP), DI get_tls(CX) // 擷取當前正在啟動並執行協程g資訊 // 將其狀態儲存在g.sched變數 MOVQ g(CX), AX // save state in g->sched MOVQ 0(SP), BX // caller's PC MOVQ BX, (g_sched+gobuf_pc)(AX) LEAQ fn+0(FP), BX // caller's SP MOVQ BX, (g_sched+gobuf_sp)(AX) MOVQ AX, (g_sched+gobuf_g)(AX) MOVQ BP, (g_sched+gobuf_bp)(AX) // switch to m->g0 & its stack, call fn MOVQ g(CX), BX MOVQ g_m(BX), BX MOVQ m_g0(BX), SI CMPQ SI, AX // if g == m->g0 call badmcall JNE 3(PC) MOVQ $runtime·badmcall(SB), AX JMP AX MOVQ SI, g(CX) // g = m->g0 // 切換到m->g0堆棧 MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp // 參數AX為之前啟動並執行協程g PUSHQ AX MOVQ DI, DX MOVQ 0(DI), DI // 在m->g0堆棧上執行函數fn CALL DI POPQ AX MOVQ $runtime·badmcall2(SB), AX JMP AXRET
如何擷取當前協程執行資訊
前兩句理解起來可能比較晦澀:
buf+0(FP) 其實就是擷取gosave的第一個參數(gobuf地址),參考 A Quick Guide to Go’s Assembler
The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP).
LEAQ buf+0(FP), BX則是擷取到第一個參數的儲存地址,而根據golang的堆棧布局,這個地址其實是調用者的sp,如下:
接下來的幾句比較容易理解,在第一句擷取了gobuf的地址後,接下來將一些相關成員設定成合適的value。 最關鍵的是以下幾句
get_tls(CX)MOVQ g(CX), BX MOVQ BX, gobuf_g(AX)
這幾句的作用是從TLS中擷取當前線程啟動並執行g,然後將其儲存在gobuf的成員g。
gosave()
gosave在golang協程切換時被調用,用來儲存被切換出去協程的資訊,以便在下次該協程被重新調度執行時可以快速恢複出協程的執行內容。
與協程調度相關的資料結構如下:
type g struct { stack stack stackguard0 uintptr stackguard1 uintptr ...... sched gobuf ......}// gobuf記錄與協程切換相關資訊 type gobuf struct { sp uintptr pc uintptr g guintptr ctxt unsafe.Pointer ret uintreg lr uintptr bp uintptr }
gosave是用組合語言寫的,效能比較高,但理解起來就沒那麼容易。
TODO: gosave()的調用路徑是什麼呢?
// void gosave(Gobuf*)// save state in Gobuf; setjmp TEXT runtime·gosave(SB), NOSPLIT, $0-8 MOVQ buf+0(FP), AX // gobuf LEAQ buf+0(FP), BX // caller's SP MOVQ BX, gobuf_sp(AX) MOVQ 0(SP), BX // caller's PC MOVQ BX, gobuf_pc(AX) MOVQ $0, gobuf_ret(AX) MOVQ $0, gobuf_ctxt(AX) MOVQ BP, gobuf_bp(AX) get_tls(CX) MOVQ g(CX), BX MOVQ BX, gobuf_g(AX)RET
前兩句理解起來可能比較晦澀:
buf+0(FP) 其實就是擷取gosave的第一個參數(gobuf地址),參考 A Quick Guide to Go’s Assembler
The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP).
LEAQ buf+0(FP), BX則是擷取到第一個參數的儲存地址,而根據golang的堆棧布局,這個地址其實是調用者的sp,如下:
接下來的幾句比較容易理解,在第一句擷取了gobuf的地址後,接下來將一些相關成員設定成合適的value。 最關鍵的是以下幾句
get_tls(CX)MOVQ g(CX), BX MOVQ BX, gobuf_g(AX)
這幾句的作用是從TLS中擷取當前線程啟動並執行g,然後將其儲存在gobuf的成員g。
gogo()
gogo的作用正好相反,用來從gobuf中恢複出協程執行狀態並跳轉到上一次指令處繼續執行。因此,其代碼也相對比較容易理解,我們就不過多贅述,如下:
gogo()主要的調用路徑:schedule()–>execute()–>googo()
// void gogo(Gobuf*)// restore state from Gobuf; longjmp TEXT runtime·gogo(SB), NOSPLIT, $0-8 MOVQ buf+0(FP), BX // gobufMOVQ gobuf_g(BX), DX MOVQ 0(DX), CX get_tls(CX)MOVQ DX, g(CX)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)MOVQ $0, gobuf_ret(BX)MOVQ $0, gobuf_ctxt(BX)MOVQ $0, gobuf_bp(BX)// 恢複出上一次執行指令,並跳轉至該指令處MOVQ gobuf_pc(BX), BX JMP BX
這裡最後一句跳轉至該協程被調度出的那條語句繼續執行,需要注意的是該函數不再返回調用者。