美文网首页
go scheduler 调度器

go scheduler 调度器

作者: 夜空中乄最亮的星 | 来源:发表于2021-05-15 10:30 被阅读0次

    什么是scheduler

    Go程序由两部分组成:Go ProgramRuntime

    Go程序关系图

    为什么要 scheduler

    Go scheduler 可以说是 Go 运行时的一个最重要的部分了。Runtime 维护所有的 goroutines,并通过 scheduler 来进行调度。Goroutines 和 threads 是独立的,但是 goroutines 要依赖 threads 才能执行。

    Go 程序执行的高效和 scheduler 的调度是分不开的。

    scheduler 底层原理

    有三个基础的结构体来实现 goroutines 的调度。g,m,p。
    g 代表一个 goroutine,它包含:表示 goroutine 栈的一些字段,指示当前 goroutine 的状态,指示当前运行到的指令地址,也就是 PC 值。
    m 表示内核线程,包含正在运行的 goroutine 等字段。
    p 代表一个虚拟的 Processor,它维护一个处于 Runnable 状态的 g 队列,m 需要获得 p 才能运行 g。

    核心的结构体:sched,它总览全局

    Go scheduler 的核心思想是:

    1. reuse threads;
    2. 限制同时运行(不包含阻塞)的线程数为 N,N 等于 CPU 的核心数目;
    3. 线程私有的 runqueues,并且可以从其他线程 stealing goroutine 来运行,线程阻塞后,可以将 runqueues 传递给其他线程

    GPM 模型

    Go 程序启动后,会给每个逻辑核心分配一个 P(Logical Processor);同时,会给每个 P 分配一个 M(Machine,表示内核线程),这些内核线程仍然由 OS scheduler 来调度。

    在初始化时,Go 程序会有一个 G(initial Goroutine),执行指令的单位。G 会在 M 上得到执行,内核线程是在 CPU 核心上调度,而 G 则是在 M 上进行调度。

    Go scheduler 会启动一个后台线程 sysmon,用来检测长时间(超过 10 ms)运行的 goroutine,将其调度到 global runqueues。这是一个全局的 runqueue,优先级比较低,以示惩罚

    两个重要的组件:
    全局可运行队列(GRQ) : GRQ 存储全局的可运行 goroutine,这些 goroutine 还没有分配到具体的 P,优先级较低
    本地可运行队列(LRQ) : 存储本地(也就是具体的 P)的可运行 goroutine

    Go scheduler 是 Go runtime 的一部分,它内嵌在 Go 程序里,和 Go 程序一起运行。因此它运行在用户空间,在 kernel 的上一层。和 Os scheduler 抢占式调度(preemptive)不一样,Go scheduler 采用协作式调度(cooperating)。

    协作式调度一般会由用户设置调度点,例如 python 中的 yield 会告诉 Os scheduler 可以将我调度出去了。这也就是为啥很多人称之为协程了.

    但是由于在 Go 语言里,goroutine 调度的事情是由 Go runtime 来做,并非由用户控制,所以我们依然可以将 Go scheduler 看成是抢占式调度,因为用户无法预测调度器下一步的动作是什么。

    和线程类似,goroutine 的状态也是三种(简化版的):


    goroutine 的状态 GPM 的工作流示意图

    实际上,Go scheduler 每一轮调度要做的工作就是找到处于runnable 的 goroutines,并执行它。找的顺序如下:

    runtime.schedule() {
        // only 1/61 of the time, check the global runnable queue for a G.  仅1/61的几率检查GRQ
        // if not found, check the local queue.  若GRQ为空,则检查LRQ
        // if not found, 如果都没有
        //     try to steal from other Ps.  就从其他P中偷,偷一半过来
        //     if not, check the global runnable queue.  还是没有, 检查全局运行队列
        //     if not found, poll network. 还是没有, 轮询网络
    }
    
    1. 1/61 的几率在全局队列中找 G,60/61 的几率在本地队列找 G;
    2. 如果全局队列找不到 G,从 P 的本地队列找 G;
    3. 如果找不到,从其他 P 的本地队列中窃取 G;
    4. 如果找不到,则从全局队列中拿取一部分 G 到本地队列。这里拿取的 “一部分” 满足一个公式:
      n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))
      注:这里 GQ 表示全局队列。
    5. 如果找不到,从网络中 poll G。

    相关文章

      网友评论

          本文标题:go scheduler 调度器

          本文链接:https://www.haomeiwen.com/subject/bjehjltx.html