38

Go 调度模型 GPM

 4 years ago
source link: https://studygolang.com/articles/29179
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GPM 模型

[TOC]

参考: 深入Golang调度器之GMP模型

前言

在了解 Go 的 gorutine 时,我们还是得先复习下,并发和并行的区别:

  • 并发: 同一段 时间执行多个任务(你同时和两个女朋友聊天)。
  • 并行: 同一时刻 执行多个任务(你和你朋友都在和你女朋友聊天)。

在单核处理器上,通过多线程共享CPU时间片串行执行(并发非并行)。而并行则依赖于多核处理器等物理资源,让多个任务可以实现并行执行(并发且并行)。

一、GPM的基本流程

1.1 GPM的含义

  • G,表示一个 goroutine,即我需要分担出去的任务;
  • P,一个装满 G 的队列,用于维护一些任务;
  • M,一个操作器,用于将一个 G 搬到线程上执行;

1.2 Go调度器基本调度过程

  1. 创建一个 G 对象;
  2. 将 G 保存至 P中;
  3. P 去唤醒(告诉)一个 M,然后继续执行它的执行序(分配下一个 G);
  4. M 寻找空闲的 P,读取该 P 要分配的 G;
  5. 接下来 M 执行一个调度循环,调用 G → 执行 → 清理线程 → 继续找新的 G 执行。

简单叙述各自的任务:

  • G,携带任务;
  • P,分配任务;
  • M,寻找任务;

二、全面的流程

2.1 各自携带的信息

  • G

    • 需执行函数的指令(指针)
    • 线程上下文的信息(goroutine切换时,用于保存 g 的上下文,例如,变量、相关信息等)
    • 现场保护和现场恢复(用于全局队列执行时的保护)
    • 所属的函数栈
    • 当前执行的 m
    • 被阻塞的时间
  • P,P/M需要进行绑定,构成一个执行单元。P决定了同时可以并发任务的数量,可通过GOMAXPROCS限制同时执行用户级任务的操作系统线程。可以通过runtime.GOMAXPROCS进行指定。

    • 状态(空闲、运行...)
    • 关联的 m
    • 可运行的 goroutine 的队列
    • 下一个 g
  • M,所有M是有线程栈的。如果不对该线程栈提供内存的话,系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。

    • 所属的调度栈
    • 当前运行的 g
    • 关联的 p
    • 状态

以上列举了三个结构各自的重要属性,现在我们来看下详细的运行流程。

2.2 准备知识

2.2.1 栈

普通栈:普通栈指的是需要调度的 goroutine 组成的函数栈,是可增长的栈,因为 goroutine 可以越开越多。

线程栈:线程栈是由需要将 goroutine 放置线程上的 m 们组成,实质上 m 也是由 goroutine 生成的,线程栈大小固定(设置了 m 的数量)。所有调度相关的代码,会先切换到该goroutine的栈中再执行。也就是说线程的栈也是用的g实现,而不是使用的OS的。

2.2.2 队列

全局队列:该队列存储的 G 将被所有的 M 全局共享,为保证数据竞争问题,需加锁处理。

本地队列:该队列存储数据资源相同的任务,每个本地队列都会绑定一个 M ,指定其完成任务,没有数据竞争,无需加锁处理,处理速度远高于全局队列。

2.2.3 上下文切换

简单理解为当时的环境即可,环境可以包括当时程序状态以及变量状态。

对于代码中某个值说,上下文是指这个值所在的局部(全局)作用域对象。相对于进程而言,上下文就是进程执行时的环境,具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存(堆栈)信息等。

2.2.4 线程清理

由于每个P都需要绑定一个 M 进行任务执行,所以当清理线程的时候,只需要将 P 释放(解除绑定)(M就没有任务),即可。P 被释放主要由两种情况:

  • 主动释放:最典型的例子是,当执行G任务时有系统调用,当发生系统调用时M会处于阻塞状态。调度器会设置一个超时时间,当超时时会将P释放。
  • 被动释放:如果发生系统调用,有一个专门监控程序,进行扫描当前处于阻塞的P/M组合。当超过系统程序设置的超时时间,会自动将P资源抢走。去执行队列的其它G任务。

阻塞是正在运行的线程没有运行结束,暂时让出 CPU。

2.2.5 抢占式调度

runtime.main 中会创建一个额外m运行 sysmon 函数,抢占就是在sysmon中实现的。

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

抢占条件:

  1. 如果 P 在系统调用中,且时长已经过一次 sysmon 后,则抢占;

调用 handoffp 解除 M 和 P 的关联。

  1. 如果 P 在运行,且时长经过一次 sysmon 后,并且时长超过设置的阻塞时长,则抢占;

设置标识,标识该函数可以被中止,当调用栈识别到这个标识时,就知道这是抢占触发的, 这时会再检查一遍是否要抢占。

2.3 详细流程

基本流程和上面一样。每创建出一个 g,优先创建一个 p 进行存储,当 p 达到限制后,则加入状态为 waiting 的队列中。

如果 g 执行时需要被阻塞,则会进行上下文切换,系统归还资源后,再返回继续执行。

当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M(抢占式调度)。

P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务(所以需要单独存储下一个 g 的地址,而不是从队列里获取)。

三、总结

相比大多数并行设计模型,Go比较优势的设计就是P上下文这个概念的出现,如果只有G和M的对应关系,那么当G阻塞在IO上的时候,M是没有实际在工作的,这样造成了资源的浪费,没有了P,那么所有G的列表都放在全局,这样导致临界区太大,对多核调度造成极大影响。

而goroutine在使用上面的特点,感觉既可以用来做密集的多核计算,又可以做高并发的IO应用,做IO应用的时候,写起来感觉和对程序员最友好的同步阻塞一样,而实际上由于runtime的调度,底层是以同步非阻塞的方式在运行(即IO多路复用)。

所以说保护现场的抢占式调度和G被阻塞后传递给其他m调用的核心思想,使得goroutine的产生。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK