goroutine 切换的时候发生了什么?




跟进去,看 Gosched 的源码:

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {


// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.
// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.
// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.
// mcall can only be called from g stacks (not g0, not gsignal).
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.
func mcall(fn func(*g))

发现mcall的作用是从g切到g0,然后执行fn(g)。这篇文章 里说过,g0是绑定在m上的一个g,使用系统栈。

我们接下来跟 gosched_m:

// Gosched continuation on g0.
func gosched_m(gp *g) {
    if trace.enabled {

然后是 goschedImpl(gp):

func goschedImpl(gp *g) {
    status := readgstatus(gp)
    if status&^_Gscan != _Grunning {
        throw("bad g status")
    casgstatus(gp, _Grunning, _Grunnable)



  • 读取当前g的状态,将状态从 _Grunning 切换成 _Grunnable
  • 解除当前g和m的关系
  • 锁定全局调度器
  • 将这个g丢到全局g队列去
  • 解锁全局调度器
  • 调用 schedule 去寻找可执行的g

关于 schedule 的分析,看 这篇文章


如果你跟进了 schedule,会发现,找到了g之后,会执行 execute 函数:

// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.
// Write barriers are allowed because this is called immediately after
// acquiring a P in several places.
func execute(gp *g, inheritTime bool) {
    _g_ := getg()

    casgstatus(gp, _Grunnable, _Grunning)
    gp.waitsince = 0
    gp.preempt = false
    gp.stackguard0 = gp.stack.lo + _StackGuard
    if !inheritTime {
    _g_.m.curg = gp
    gp.m = _g_.m

    // Check whether the profiler needs to be turned on or off.
    hz := sched.profilehz
    if _g_.m.profilehz != hz {

    if trace.enabled {
        // GoSysExit has to happen when we have a P, but before GoStart.
        // So we emit it here.
        if gp.syscallsp != 0 && gp.sysblocktraced {


然后继续跟进 gogo:

func gogo(buf *gobuf)

发现是汇编写的,那我们搜索一下,然后跳到 amd64 版本的:

// void gogo(Gobuf*)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
    MOVQ    buf+0(FP), BX       // gobuf
    MOVQ    gobuf_g(BX), DX
    MOVQ    0(DX), CX       // make sure g != nil
    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)    // 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

即把对应的寄存器的值刷成要执行的g的值,如SP,PC等。可以看看 gobuf 是啥:

type gobuf struct {
    // The offsets of sp, pc, and g are known to (hard-coded in) libmach.
    // ctxt is unusual with respect to GC: it may be a
    // heap-allocated funcval, so GC needs to track it, but it
    // needs to be set and cleared from assembly, where it's
    // difficult to have write barriers. However, ctxt is really a
    // saved, live register, and we only ever exchange it between
    // the real register and the gobuf. Hence, we treat it as a
    // root during stack scanning, which means assembly that saves
    // and restores it doesn't need write barriers. It's still
    // typed as a pointer so that any other writes from Go get
    // write barriers.
    sp   uintptr
    pc   uintptr
    g    guintptr
    ctxt unsafe.Pointer
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr // for GOEXPERIMENT=framepointer