返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.5.1 调度

发布于 2024-10-12 19:16:06 字数 5557 浏览 0 评论 0 收藏 0

当 MP 启动后,开始执行任务调度,这是一个由四个函数组成的循环。

  • schedule : 核心函数。检查相关状态(比如 GC),查找 runnable G。
  • execute : 执行函数。通过汇编函数 gogo 切换到 G.stack,执行用户函数。
  • G.fn : 用户函数。由 go func 传递而来。
  • goexit : 退出函数。清理执行现场,重新进入 scheule 循环。

除 G.fn 外,调度循环在 g0 栈执行,不会向普通函数调用堆栈那样回溯。

通过提前设置 IP/PC,然后 jmp 的方式跳转。

                            +----------------+                              +---------+
  --- mstart --- M.g0 --->  | schedule       | ----------- M.g0 ----------> | execute |
                            +----------------+                              +---------+
                            |   globrunqget  |                              |   gogo  |
                            +----------------+                              +---------+
                            |   runqget      |                                   |
                            +----------------+                                   |
                            |   findrunnable |                                   |
                            +----------------+                                   |
                                  ^                                           G.stack
                                  |                                              |
                                  |                                              |
                                 M.g0                                            |
                                  |                                              |
                                  |                                              v
                            +-----------+                                     +------+
                            | goexit1   | <------------- M.g0 --------------- | G.fn |
                            +-----------+                                     +------+
         ........ mexit ... |   goexit0 | 
                            +-----------+
// proc.go

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.

func schedule() {
    
    // 检查当前 M 是否被锁定到特定 G。
    if _g_.m.lockedg != 0 {
        stoplockedm()
        execute(_g_.m.lockedg.ptr(), false)    // Never returns.
    }
    
top:
    
    // 检查 STW !!!
    if sched.gcwaiting != 0 {
        gcstopm()
        goto top
    }
    
    // 垃圾回收启动标记。
    if gp == nil && gcBlackenEnabled != 0 {
        gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
    }
    
    // 每隔 60 次(任务执行计数器),执行一次全局队列任务,确保公平。
    if gp == nil {
        if _g_.m.p.ptr().schedtick % 61 == 0 && sched.runqsize > 0 {
            gp = globrunqget(_g_.m.p.ptr(), 1)
        }
    }
    
    // 查找本地任务。
    if gp == nil {
        gp, inheritTime = runqget(_g_.m.p.ptr())
    }
    
    // 从不同途径获取任务。
    // 如没有找到,则阻塞。
    if gp == nil {
        gp, inheritTime = findrunnable()    // blocks until work is available
    }
    
    // 找到,解除自旋状态。
    if _g_.m.spinning {
        resetspinning()
    }
    
    // 检查 G 是否被绑定到特定 M。
    if gp.lockedm != 0 {
        startlockedm(gp)
        goto top
    }
    
    // 执行任务。
    execute(gp, inheritTime)
}

如返回的是 P.runnext,那么 inheritTime = true ,执行计数器(P.schedtick)不会增加。

这表示继承上个任务时间片,减少参与全局队列任务的次数。

另一方面,sysmon 通过比对两个不同时段的计数来判断 P 是否运行了新任务。

如没有,则检查执行时间是否过长(forcePreemptNS = 10ms),从而决定是否抢占调度。

从这点上说,runnext 继承时间片,延长执行时间,却没有增加计数,会提高抢占调度几率。

GODEBUG=scheddetail 输出里可以看到计数器信息。

单个 G 未必在一次调度内执行结束。因某些原因中断,被重新放回任务队列,等待断点恢复。

MP 切换任务前,会将执行状态(PC、SP)保存到 G.sched,其他寄存器数据保存到 G.stack。

切换本身(G100->G0->G101)消耗并不大,关键在于如何查找下一个 G 任务。

  • 阻塞:block,syscall、mutex、channel。
  • 抢占:preempt,sysmon。

locked

某些情况(syscall、cgo)下,G 会锁定 M。

一旦锁定,M 会休眠直到 G 出现,并执行。

同理,G 只能在锁定 M 上执行。

这么做是为了维持 M 的某些特殊状态(比如 signal 等)。

// LockOSThread wires the calling goroutine to its current operating system thread.
// The calling goroutine will always execute in that thread, and no other goroutine will execute in it,
// until the calling goroutine has made as many calls to UnlockOSThread as to LockOSThread.
// If the calling goroutine exits without unlocking the thread, the thread will be terminated.

func LockOSThread() {
    dolockOSThread()
}
func dolockOSThread() {
    _g_ := getg()
    _g_.m.lockedg.set(_g_)
    _g_.lockedm.set(_g_.m)
}

如当前 M 被某个 G 锁定,那么会将 P 交给其他 M,自己休眠等待女盆友出现。

为什么不自己找?而是休眠?

女盆友不知道在哪个队列,且按照队列顺序不知道什么时候轮到她。

如此,还不如休眠,等她自己出现。

// Stops execution of the current m that is locked to a g until the g is runnable again.
// Returns with acquired P.

func stoplockedm() {
    _g_ := getg()
    
    // 交出当前 P,让其他 M 匹配。
    if _g_.m.p != 0 {
        _p_ := releasep()
        handoffp(_p_)
    }
    
    // 等待目标 G 出现。
    notesleep(&_g_.m.park)
    noteclear(&_g_.m.park)
    
    // 绑定 P,回到 schedule,使用 execute 执行。
    acquirep(_g_.m.nextp.ptr())
    _g_.m.nextp = 0
}

如 M 遇到别人女盆友,那么将 P 让给其男友,自己去休眠。

// Schedules the locked m to run the locked gp.

func startlockedm(gp *g) {
    
    // 该 G 的男盆友 M。
    mp := gp.lockedm.ptr()
    
    // 当前 M 将 P 移交给其男友,并唤醒。
    _p_ := releasep()
    mp.nextp.set(_p_)
    notewakeup(&mp.park)
    
    // 当前 M 休眠。
    stopm()
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文