美文网首页
Go调度相关

Go调度相关

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

    go 调度
    go routinue在线程中进行调度

    GPM的概念:

    • G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。
    • M(Machine): 工作线程,在Go中称为Machine。
    • P(Processor): 逻辑处理器(Go中定义的一个摡念,不是指CPU),包含运行Go代码的必要资源,也有调度goroutine的能力。

    GPM的关系:

    1.M必须拥有P才可以执行G中的代码,交给线程进行执行。默认情况下等同于CPU的核数。一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个
    2.P含有一个包含多个G的队列,调度G交由M执行。默认情况下等同于CPU的核数
    3.P可以调度G交由M执行

    调度算法

    image
    • G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。

    • P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256。

    • M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。

    • 每个 P 维护一个 G 的本地队列;

    • 当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的本地可执行队列中,如果满了则放入Global;

    • 当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 就随机选择另外一个 P,从其可执行的 G 队列中取走一半。

    调度过程

    当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。执行调度算法:当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P 也不会就这么在那划水啥都不干,它会先尝试从 Global 队列寻找 G 来执行,如果 Global 队列为空,它会随机挑选另外一个 P,从它的队列里中拿走一半的 G 到自己的队列中执行。

    G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:

    1.如果有空闲的P,则获取一个P,继续执行G0。
    2.如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

    调度策略:

    1.队列轮转
    2.系统调用
    3.工作量窃取

    go routine vs thread

    内存占用:
    1.goroutine 的栈内存消耗为 2 KB,实际运行过程中,如果栈空间不够用,会自动进行扩容,最大可达 1GB。(64 位机器最大是 1G,32 位机器最大是 256M)
    2.thread 则需要消耗 1 MB 栈内存,而且还需要一个被称为 “a guard page” 的区域用于和其他 thread 的栈空间进行隔离
    创建和销毀:
    1.goroutine 直接从runtime申请,用户级
    2.thread 需要进行系统调用
    切换:
    1.goroutines 切换只需保存三个寄存器:Program Counter, Stack Pointer and BP。
    2.threads 切换时,需要保存各种寄存器
    因此goroutines切换速度比thread快很多

    为什么要 scheduler:

    根据scheduler的功能来说明:

    1. 因为所有的ggoroutines 都通过runtime维护,而众多的goroutines需要依赖thread才能执行,因此使用scheduler来对goroutines的运行进行调度控制已达到高效的执行效率。
    2. 若没有scheduler,比如1:1模型,在同一时刻,一个线程上只能跑一个 goroutine,当 goroutine 发生阻塞,其他goroutine就会被阻塞,大大影响了性能,而有了scheduler,被阻塞的线程会被调度走,让其他的goroutine来执行。
    3. 如果非要杠,没有scheduler也能运行,但是无法达到高效的目的。

    Go scheduler的作用

    Go scheduler 的职责就是将所有处于 runnable 的 goroutines 均匀分布到在 P 上运行的 M。
    Go scheduler 每一轮调度要做的工作就是找到处于 runnable 的 goroutines,并执行它

    M:N模型 和工作窃取

    在任一时刻,M 个 goroutines(G) 要分配到 N 个内核线程(M),这些 M 跑在个数最多为 GOMAXPROCS 的逻辑处理器(P)上。每个 M 必须依附于一个 P,每个 P 在同一时刻只能运行一个 M。如果 P 上的 M 阻塞了,那它就需要其他的 M 来运行 P 的 LRQ 里的 。当 P2 上的一个 G 执行结束,它就会去 LRQ 获取下一个 G 来执行。如果 LRQ 已经空了,就是说本地可运行队列已经没有 G 需要执行,并且这时 GRQ 也没有 G 了。这时,P2 会随机选择一个 P(称为 P1),P2 会从 P1 的 LRQ “偷”过来一半的 G。

    GPM是什么

    G、P、M 是 Go 调度器的三个核心组件

    go初始化流程:

    1.call osinit。初始化系统核心数。
    2.call schedinit。初始化调度器。
    3.make & queue new G。创建新的 goroutine。
    4.call runtime·mstart。调用 mstart,启动调度。
    5.The new G calls runtime·main。在新的 goroutine 上运行 runtime.main 函数。

    scheduler初始化过程:

    1.调整 SP
    2.初始化 g0 栈
    3.主线程绑定 m0
    4.初始化 m0
    5.初始化 allp

    main goroutine是如何诞生的?

    main goroutine执行流程:

    1.创建后台监控线程sysmon
    2.runtime包初始化
    3.main包初始化
    4.执行用户main函数
    5.调用exit(0)退出

    普通 goroutine执行流程:

    先是跳转到提前设置好的 goexit 函数的第二条指令,然后调用 runtime.goexit1,接着调用 mcall(goexit0),而 mcall 函数会切换到 g0 栈,运行 goexit0 函数,清理 goroutine 的一些字段,并将其添加到 goroutine 缓存池里,然后进入 schedule 调度循环。到这里,普通 goroutine 才算完成使命

    g0栈和用户栈如何切换?

    这中间涉及到栈和寄存器的切换

    如何保存g0的调度信息?

    schedule函数有什么重要作用?
    gogo函数如何完成从g0到main goroutine的切换?

    schedule如何循环运转?

    schedule() -> execute() -> gogo() -> goroutine 任务 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()

    g0 栈的作用就是为运行 runtime 代码提供一个“环境”。

    M如何找工作

    1.先从本地队列找
    2.定期会从全局队列找
    3.最后实在没办法,就去别的 P 偷

    sysmon后台监控线程作用(执行监控任务):

    1.抢占处于系统调用的 P,让其他 m 接管它,以运行其他的 goroutine
    2.将运行时间过长的 goroutine 调度出去,给其他 goroutine 运行的机会

    sysmon函数的作用

    sysmon 执行一个无限循环,一开始每次循环休眠 20us,之后(1 ms 后)每次休眠时间倍增,最终每一轮都会休眠 10ms;
    sysmon 中会进行 netpool(获取 fd 事件)、retake(抢占)、forcegc(按时间强制执行 gc),scavenge heap(释放自由列表中多余的项减少内存占用)等处理

    相关文章

      网友评论

          本文标题:Go调度相关

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