GMP模型(四)G的主动挂起状态

继续看g,在上一章中我们了解了golang如何解决g中代码存在系统调用的问题,接下来继续看另外一种情况,当g中存在锁或者类似需要waiting状态的时候,需要怎么处理?

其实简单想一想也能明白,无非是将g暂时挂起,让p继续调度其他g,等g执行完毕之后,再将g加入待运行队列即可,不过这里我们还是通过代码分析,补全我们的g状态流程图。

gopark()

g需要等待某个条件达成时,例如sync.Lock、sync.WaitGroup等,则会调用gopark(),其源码如下:

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
    if reason != waitReasonSleep {
        checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
    }
  //锁定当前m,禁止抢占
    mp := acquirem()
  //获取当前运行的g
    gp := mp.curg
  //检查当前g的状态
    status := readgstatus(gp)
    if status != _Grunning && status != _Gscanrunning {
        throw("gopark: bad g status")
    }

    mp.waitlock = lock
    mp.waitunlockf = unlockf
    gp.waitreason = reason
    mp.waittraceev = traceEv
    mp.waittraceskip = traceskip
    releasem(mp)
    //通过mcall调用 park_m
    mcall(park_m)
}

mcall的主要作用是切换到g0执行传入的func(),一般用于当前g不需要继续执行,一般传入的func()最终都会通过调用schedule()结束,也就是这个方法不会再返回。继续看park_m()

func park_m(gp *g) {
  //获取当前的g
    _g_ := getg()

    if trace.enabled {
        traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
    }

  //将g的状态修改为_Gwaiting
    casgstatus(gp, _Grunning, _Gwaiting)
  //解除g与m的绑定
    dropg()

  //如果需要执行waitunlockf 
    if fn := _g_.m.waitunlockf; fn != nil {
    //执行相关方法
        ok := fn(gp, _g_.m.waitlock)
        _g_.m.waitunlockf = nil
        _g_.m.waitlock = nil
    //如果waitunlockf 返回ok,则g不休眠,直接继续运行
        if !ok {
            if trace.enabled {
                traceGoUnpark(gp, 2)
            }
            casgstatus(gp, _Gwaiting, _Grunnable)
            execute(gp, true) // Schedule it back, never returns.
        }
    }
  //继续执行调度
    schedule()
}

可以看到park_m和我们预想的一样,就是简单的把gm解绑,然后将g的状态修改为_Gwaiting,然后继续调度其他g.

goready()

当某个条件达成时,则可以调用gpready()将对应的g恢复运行,其具体原理是修改g的状态,并将其加入运行队列,等待调度即可。

//runtime.proc.go 
func goready(gp *g, traceskip int) {
    systemstack(func() {
        ready(gp, traceskip, true)
    })
}
func ready(gp *g, traceskip int, next bool) {
    if trace.enabled {
        traceGoUnpark(gp, traceskip)
    }

  //检查当前g的状态
    status := readgstatus(gp)


    _g_ := getg()
    mp := acquirem() 
    if status&^_Gscan != _Gwaiting {
        dumpgstatus(gp)
        throw("bad g->status in ready")
    }

    //修改状态为_Grunnable
    casgstatus(gp, _Gwaiting, _Grunnable)
  //放进p的待运行队列
    runqput(_g_.m.p.ptr(), gp, next)
  //尝试唤醒m
    wakep()
    releasem(mp)
}

至此,我们可以继续补充g的状态图:

image-20220918232725698

从这里可以看出来WaitingSyscall的区别在于Waiting是主动挂起,而Syscall是被动阻塞,主动挂起不需要占用m,因此可以直接将自己踢出队列即可,而被动阻塞则需要处理mp的关系,在调用前需要解绑p,避免阻塞p的其他g的执行,调用完毕之后,需要再去寻找p进行绑定,以便继续调度。

到这里,和调度相关的GMP模型中的G已经分析完毕,对于G相关的问题我们也基本完全解答。后面几篇中,我们将继续分析MP的状态变化。