前面介绍的 scheduler 和 channel 里面都与 gopark 和 goready 这两个函数紧密相关,但是站在上层可以理解这两个函数的作用,但是出于对源码探索,我们要明白这两个函数不仅仅做了啥,还要知道怎么做的。本文主要内容是从底层源码分析这两个函数原理:

    1. gopark 函数
    2. goready 函数

    gopark 函数在协程的实现上扮演着非常重要的角色,用于协程的切换,协程切换的原因一般有以下几种情况:

    1. 系统调用;
    2. channel 读写条件不满足;
    3. 抢占式调度时间片结束;

    gopark 函数做的主要事情分为两点:

    1. 解除当前 goroutine 的 m 的绑定关系,将当前 goroutine 状态机切换为等待状态;
    2. 调用一次 schedule() 函数,在局部调度器 P 发起一轮新的调度。

    下面我们来研究一下 gopark 函数是怎么实现协程切换的。

    先看看源码:

    1. func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
    2. if reason != waitReasonSleep {
    3. checkTimeouts()
    4. }
    5. mp := acquirem()
    6. gp := mp.curg
    7. status := readgstatus(gp)
    8. if status != _Grunning && status != _Gscanrunning {
    9. throw("gopark: bad g status")
    10. }
    11. mp.waitlock = lock
    12. mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))
    13. gp.waitreason = reason
    14. mp.waittraceev = traceEv
    15. mp.waittraceskip = traceskip
    16. releasem(mp)
    17. mcall(park_m)
    18. }

    源码里面最重要的一行就是调用 mcall(park_m) 函数,park_m 是一个函数指针。mcall 在 golang 需要进行协程切换时被调用,做的主要工作是:

    1. 切换当前线程的堆栈从 g 的堆栈切换到 g0 的堆栈;
    2. 并在 g0 的堆栈上执行新的函数 fn(g);
    3. 保存当前协程的信息 (PC/SP 存储到 g->sched),当后续对当前协程调用 goready 函数时候能够恢复现场;

    mcall 函数执行原理

    mcall 的函数原型是:

    1. func mcall(fn func(*g))

    这里函数 fn 的参数 g 指的是在调用 mcall 之前正在运行的协程。

    我们前面说到,mcall 的主要作用是协程切换,它将当前正在执行的协程状态保存起来,然后在 m->g0 的堆栈上调用新的函数。 在新的函数内会将之前运行的协程放弃,然后调用一次 schedule() 来挑选新的协程运行。(也就是在 fn 函数里面会调用一次 schedule() 函数进行一次 scheduler 的重新调度,让 m 去运行其余的 goroutine)

    mcall 函数是通过汇编实现的,在 asm_amd64.s 里面有 64 位机的实现,源码如下:

    1. // func mcall(fn func(*g))
    2. // Switch to m->g0's stack, call fn(g).
    3. // Fn must never return. It should gogo(&g->sched)
    4. // to keep running g.
    5. TEXT runtime·mcall(SB), NOSPLIT, $0-8
    6. //DI中存储参数fn
    7. MOVQ fn+0(FP), DI
    8. get_tls(CX)
    9. // 获取当前正在运行的协程g信息
    10. // 将其状态保存在g.sched变量
    11. MOVQ g(CX), AX // save state in g->sched
    12. MOVQ 0(SP), BX // caller's PC
    13. MOVQ BX, (g_sched+gobuf_pc)(AX)
    14. LEAQ fn+0(FP), BX // caller's SP
    15. MOVQ BX, (g_sched+gobuf_sp)(AX)
    16. MOVQ AX, (g_sched+gobuf_g)(AX)
    17. MOVQ BP, (g_sched+gobuf_bp)(AX)
    18. // switch to m->g0 & its stack, call fn
    19. MOVQ g(CX), BX
    20. MOVQ g_m(BX), BX
    21. MOVQ m_g0(BX), SI
    22. CMPQ SI, AX // if g == m->g0 call badmcall
    23. JNE 3(PC)
    24. MOVQ $runtime·badmcall(SB), AX
    25. JMP AX
    26. MOVQ SI, g(CX) // g = m->g0
    27. // 切换到m->g0堆栈
    28. MOVQ (g_sched+gobuf_sp)(SI), SP // sp = m->g0->sched.sp
    29. // 参数AX为之前运行的协程g
    30. PUSHQ AX
    31. MOVQ DI, DX
    32. MOVQ 0(DI), DI
    33. // 在m->g0堆栈上执行函数fn
    34. CALL DI
    35. POPQ AX
    36. MOVQ $runtime·badmcall2(SB), AX
    37. JMP AX
    38. RET

    上面的汇编代码我也不是很懂,但是能够大致能够推断出主要做的事情:

    1. 保存当前 goroutine 的状态 (PC/SP) 到 g->sched 中,方便下次调度;
    2. 切换到 m->g0 的栈;
    3. 然后 g0 的堆栈上调用 fn;

    回到 gopark 函数里面,我们知道 mcall 会切换到 m->g0 的栈,然后执行 park_m 函数,下面看一下 park_m 函数源码:

    1. func park_m(gp *g) {
    2. _g_ := getg()
    3. if trace.enabled {
    4. traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
    5. }
    6. casgstatus(gp, _Grunning, _Gwaiting)
    7. dropg()
    8. if _g_.m.waitunlockf != nil {
    9. fn := *(*func(*g, unsafe.Pointer) bool)(unsafe.Pointer(&_g_.m.waitunlockf))
    10. ok := fn(gp, _g_.m.waitlock)
    11. _g_.m.waitunlockf = nil
    12. _g_.m.waitlock = nil
    13. if !ok {
    14. if trace.enabled {
    15. traceGoUnpark(gp, 2)
    16. }
    17. casgstatus(gp, _Gwaiting, _Grunnable)
    18. execute(gp, true)
    19. }
    20. }
    21. schedule()
    22. }

    park_m 函数主要做的几件事情就是:

    1. 线程安全更新 goroutine 的状态,置为_Gwaiting 等待状态;
    2. 解除 goroutine 与 OS thread 的绑定关系;
    3. 调用 schedule() 函数,调度器会重新调度选择一个 goroutine 去运行;

    schedule 函数里面主要调用路径就是:

    1. schedule()–>execute()–>gogo()

    gogo 函数的作用正好相反,用来从 gobuf 中恢复出协程执行状态并跳转到上一次指令处继续执行。因此,其代码也相对比较容易理解,当然,其实现也是通过汇编代码实现的。

    goready 函数相比 gopark 函数来说简单一些,主要功能就是唤醒某一个 goroutine,该协程转换到 runnable 的状态,并将其放入 P 的 local queue,等待调度。

    1. func goready(gp *g, traceskip int) {
    2. systemstack(func() {
    3. ready(gp, traceskip, true)
    4. })
    5. }

    该函数主要就是切换到 g0 的栈空间然后执行 ready 函数。

    下面我们看看 ready 函数源码 (删除非主流程代码):

    1. func ready(gp *g, traceskip int, next bool) {
    2. status := readgstatus(gp)
    3. _g_ := getg()
    4. _g_.m.locks++
    5. if status&^_Gscan != _Gwaiting {
    6. dumpgstatus(gp)
    7. throw("bad g->status in ready")
    8. }
    9. casgstatus(gp, _Gwaiting, _Grunnable)
    10. runqput(_g_.m.p.ptr(), gp, next)
    11. if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {
    12. wakep()
    13. }
    14. _g_.m.locks--
    15. if _g_.m.locks == 0 && _g_.preempt {
    16. _g_.stackguard0 = stackPreempt
    17. }
    18. }

    代码的核心流程最主要工作就是将 gp(goroutine) 的状态机切换到 runnnable,然后加入到 P 的局部调度器的 local queue,等待 P 进行调度。

    所以这里有一点需要我们注意到的是,对一个协程调用 goready 函数,这个协程不是可以马上就执行的,而是要等待调度器的调度执行。
    https://blog.csdn.net/u010853261/article/details/85887948