g0是Go语言中一种特殊协程,每个M都拥有一个g0协程,主要负责当前M上用户协程的调度工作,在执行runtime.newm()
函数创建M时,会同时初始化g0协程。
g0协程执行时使用系统栈,每个线程从启动开始往下执行,都属于g0的工作范围,直到运行至runtime.gogo()
启动用户协程,g0的工作才算暂时告一段落。
如图所示,Go语言中的协程调度总是不断地经历着类似的循环,以runtime.Gosched()
让出调度的流程举例。
当M创建之后,会同时初始化g0,运行在不同的平台上时,程序兼容各平台特性,最终执行到runtime.mstart()
正式启动g0的调度逻辑,再经过runtime.mstart1()
进入到调度循环中,由runtime.gogo()
启动用户协程,在用户逻辑中执行runtime.Gosched()
让出执行权限,再经过runtime.mcall()
回到g0的工作范围(这条流程下,刚刚执行过的协程会被放到全局队列中)。
反复重用的栈帧
调度技术上,有不少细节的设计很有技巧,其中一个就是对于g0栈帧的管理。
g0采用的是无限递归的方式重复运行调度逻辑,正常的程序执行路径都是保存在栈内,按正常程序的编译结果,g0的协程栈会无限膨胀直到移除,因此Go语言对这里进行了一些优化,当执行到runtime.mstart1()
时,g0会提前计算出一份协程栈的地址数据,保存在g0的结构中。
_g_.sched.pc = getcallerpc()
_g_.sched.sp = getcallersp()
当g0执行到runtime.gogo()
切换至用户协程时,并不会将当前g0的栈帧保存起来,而当用户协程经过runtime.mcall()
回到g0时,再次加载的还是最初在runtime.mstart1()
中计算出来的栈地址数据,通过这种技巧,反复将栈顶指针重置,从而实现栈帧的重复使用。
TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT, $0-8
MOVQ AX, DX DX = fn
// save state in g->sched
MOVQ 0(SP), BX // caller's PC
MOVQ BX, (g_sched+gobuf_pc)(R14)
LEAQ fn+0(FP), BX // caller's SP
MOVQ BX, (g_sched+gobuf_sp)(R14)
MOVQ BP, (g_sched+gobuf_bp)(R14)
// switch to m->g0 & its stack, call fn
MOVQ g_m(R14), BX
MOVQ m_g0(BX), SI // SI = g.m.g0
CMPQ SI, R14 // if g == m->g0 call badmcall
JNE goodm
JMP runtime·badmcall(SB)
goodm:
MOVQ R14, AX // AX (and arg 0) = g
MOVQ SI, R14 // g = g.m.g0
get_tls(CX) // Set G in TLS
MOVQ R14, g(CX)
MOVQ (g_sched+gobuf_sp)(R14), SP // sp = g0.sched.sp
PUSHQ AX // open up space for fn's arg spill slot
MOVQ 0(DX), R12
CALL R12 // fn(g)
POPQ AX
JMP runtime·badmcall2(SB)
RET
优先提高并行度
Worker角色的M被创建时,默认都需要再创建另一个新的M,直至没有空闲的p为止。
这块的处理流程是每创建一个M,在执行调度时,如果当前M中spinning
成员被标记为true
,则尝试创建另一个M,采用全局变量sched.nmspinning
控制创建流程,实现M的顺序创建。