此篇简要介绍Go语言调度器的GMP模型内容。

并发和并行的概念

并发

逻辑上具有处理多个同时性任务的能力。

并行

物理上同一时刻执行多个并发任务。

支持高并发的模型

CSP

消息之间通过channel发送,发送者和接收者不必关心,松耦合。

Actor

Actor是基本的处理单元,相互之间直接发送消息,需要知道彼此的地址。

MPG模型

M

  1. M即Machine或称为工作线程,所有M是有线程栈的。每个M都代表了1个内核线程,相当于内核线程在 Go 进程中的映射。OS调度器负责把内核线程分配到CPU的核上执行。
  2. M必须和一个P关联才能运行G。
  3. work stealing:当M绑定的P没有可运行的G时,它可以从其他运行的M那里偷取G。
  4. 线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去

P

  1. P即Processor是一个抽象的概念,并不是真正的物理CPU。它包含了运行goroutine的资源。
  2. 如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
  3. P需要和M进行绑定,构成一个执行单元。
  4. P决定了同时可以并发任务的数量,可通过runtime.GOMAXPROCS限制同时执行用户级任务的操作系统线程。在Go1.5之后GOMAXPROCS被默认设置可用的核数,而之前则默认为1。所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个。
  5. P 的个数是通过 runtime.GOMAXPROCS 设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些 P 和 M ,但不会太多,切换太频繁的话得不偿失。
  6. P有两种队列:本地队列(Local Queue)和全局队列(Global Queue)。

本地队列: 当前P的队列,本地队列是Lock-Free,没有数据竞争问题,无需加锁处理,可以提升处理速度。同全局队列类似,存放的也是等待运行的G,存的数量有限。
全局队列:全局队列为了保证多个P之间任务的平衡。所有M共享P全局队列,为保证数据竞争问题,需要加锁处理。相比本地队列,处理速度要低。
一个 Prcessor 表示执行 Go 代码片段的所必需的上下文环境,可以理解为用户代码逻辑的处理器。

G

  1. G即Goroutine的缩写。
  2. Go不同版本Goroutine默认栈大小不同。
  3. 在Go中,线程是运行Goroutine的实体,调度器的功能是把可运行的Goroutine分配到工作线程上。

MPG的调度过程

  1. 首先通过执行go func()来创建一个G对象,新建的G对象会被保存到P的本地队列或者是全局队列(注意这里的P指的是创建G的P)。P此时去唤醒或创建一个M来执行G。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。
  2. M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的寄存器进行现场恢复(从上次中断位置继续执行)。
  3. 每一个 M 都会以一个内核线程绑定,M 和 P 之间也是一对一的关系,而 P 和 G 的关系则是一对多。在运行过程中,M 和 内核线程之间对应关系的不会变化,在 M 的生命周期内,它只会与一个内核线程绑定,而 M 和 P 以及 P 和 G 之间的关系都是动态可变的。
  4. 在实际的运行过程中,M 和 P 的组合才能够为 G 提供有效的运行环境,而多个可执行 G 将会顺序排成一个队列挂在某个 P 上面,等待调度和执行。
  5. M 的创建一般是因为没有足够的 M 来和 P 组合以为 G 提供运行环境,在很多时候 M 的数量可能会比 P 要多。在单个 Go 进程中,P 的最大数量决定了程序的并发规模,且 P 的最大数量是由程序决定的。可以通过修改环境变量 GOMAXPROCS 和 调用函数 runtime.GOMAXPROCS 来设定 P 的最大值。
  6. M 和 P 会适时的组合和断开,保证 P 中的待执行 G 队列能够得到及时运行。比如一个 G 如果因为网络 I/O 而阻塞了 M,那么 P 就会携带剩余的 G 投入到其它 M 的怀抱中。这个新的 M 可能是新创建的,也可能是从调度器空闲 M 列表中获取的,取决于此时的调度器空闲 M 列表中是否存在多余 M,从而避免 M 的过多创建。
  7. 一言蔽之,调度的本质就是 P 将 G 合理的分配给某个 M 的过程。

其它注意

  1. 本地队列有数量限制,即不允许超过 256 个,并且在新建G时,会优先选择P的本地队列。如果本地队列满了,则将P的本地队列中一半的G移动到全局队列,这可以理解为调度资源的共享和再平衡。
  2. 其中的steal行为是用来做什么的呢?当创建新的G或者G变成可运行状态时,它会被推送并加入到当前P的本地队列中。当P执行G完毕后,它开始“干活”,它会从本地队列中弹出G,同时检查当前本地队列是否为空。如果为空,则会随机的从其它P的 本地队列 中尝试窃取 一半 可运行的G到自己的名下。