Golang协程调度GPM模型


GPM模型

GPM模型是Go语言运行时系统中用来管理和调度大量goroutine的核心机制。在这个模型里:

  • G(Goroutine):代表协程,即Go语言中的轻量级线程。它们由Go运行时自动管理,可以非常高效地创建和销毁,并且占用的资源非常少(每个大约只有几KB的栈空间)。Goroutines在用户空间内进行调度,不需要操作系统级别的上下文切换,因此性能非常高。

  • P(Processor):这里不是指物理CPU处理器,而是逻辑上的处理器概念,也就是Go运行时中的“处理器”。P包含了一些必要的运行环境信息,比如可运行的Goroutine队列、内存分配器状态等。每一个P都可以被看作是一个独立的任务调度单元,它可以将Goroutine分配给绑定到它的M去执行。程序启动时会根据环境变量GOMAXPROCS(默认等于系统的逻辑CPU数量)确定有多少个P会被创建并参与调度。

  • M(Machine/Thread):指的是操作系统级别的线程,也称为机器线程。每个M实际上就是一个OS线程,它负责执行一个或多个Goroutine。当一个Goroutine需要执行时,它会被分配给一个可用的M来运行。如果当前的M因为阻塞操作(如I/O等待)而无法继续执行,则会创建新的M或者复用现有的空闲M来接管未完成的工作。

  1. 全局队列(Global Queue):存放等待运行的G。全局队列可能被任意的P去获取里面的G,所以全局队列相当于整个模型中的全局资源,那么自然对于队列中的读写操作是要加入互斥动作的
  2. P的本地队列:同全局队列类似,存放的也是等待运行的G,但存放的数量有限,不能超过256个。新建G'时,G'优先加入P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列
  3. P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个
  4. M:线程想运行任务就得获取P,从P的本地队列中获取G,当P队列为空时,M也会尝试从全局队列获取一批G放到P的本地队列,或从其他P的本地队列“偷”放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去

Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行


GPM模型的工作流程

  1. 任务分配:新创建的Goroutine首先会被放入某个P的本地队列中。如果该队列满了,部分任务可能会被移动到全局队列。
  2. 任务窃取(Work Stealing):当某个M关联的P没有更多可执行的Goroutine时,它可以从其他P的本地队列中“偷取”一半的任务来执行,以此减少锁竞争并提高效率。
  3. M与P的关系:M必须持有P才能执行Goroutine。每当一个M开始执行Goroutine时,它会从对应的P那里获取下一个待执行的任务。如果所有M都忙于处理阻塞操作,那么即使有闲置的P存在,也会创建新的M来服务这些P。
  4. 抢占调度:为了防止某些长时间运行的Goroutine独占CPU资源,Go引入了抢占式调度机制,允许强制暂停过长运行的Goroutine以确保公平性。
  5. Hand-off机制:当M因Goroutine进行系统调用或其他原因而阻塞时,它会释放所持有的P,让其他空闲的M接手这个P及其上面的任务。一旦阻塞解除,原来的M会尝试重新获取一个P来继续工作。

特殊的M0和G0

  • M0:这是启动程序后创建的第一个主线程,用于执行初始化操作以及启动第一个Goroutine。
  • G0:每个M都有自己的G0,它是专门用来负责调度工作的特殊Goroutine。G0不指向任何具体的函数代码,但在调度或系统调用期间会使用其栈空间。

P队列为空的处理策略

在 Go 语言的调度器中,当一个 M(机器/线程)关联的 P(处理器)本地队列为空时,它会按照一定的策略尝试获取新的 goroutine (G) 来执行。这个过程涉及检查全局队列和其它 P 的本地队列。具体来说,Go 调度器是如何区分这两个来源的呢?

获取任务的顺序

  1. 首先检查全局队列:当 P 的本地队列为空时,M 首先会尝试从全局队列中获取任务。全局队列是一个所有 P 共享的任务池,包含了尚未分配给任何 P 的 G。

  2. 然后尝试“工作窃取”:如果全局队列也为空,那么 M 会尝试从其他 P 的本地队列中“窃取”任务。这是因为其他 P 的本地队列可能有未被执行的 G,而这些 G 可能是当前 P 可以立即执行的。

区分机制

  • 全局队列中的任务是公开的:所有 M 和 P 都可以访问全局队列,并且可以从其中获取任务。全局队列的任务通常是新创建的 G 或者是由其他 P 放弃或完成的工作后返回到全局队列的任务。

  • 工作窃取的目标是特定的:当进行工作窃取时,M 会选择一个随机的其他 P,并尝试从它的本地队列中获取一半的任务(通常是从队列的尾部)。这种选择不是随机的;它遵循一种称为“work stealing”的算法,旨在尽量减少冲突和提高缓存局部性。

实现细节

在实际实现中,Go 的调度器使用了锁和原子操作来确保对全局队列和其他 P 的本地队列的安全访问。例如:

  • 全局队列的操作通常涉及到锁或者无锁的数据结构,以保证多线程环境下的安全性。
  • 工作窃取可能会使用原子操作来安全地从另一个 P 的本地队列中移除任务,而不需要完全锁定整个队列,这样可以减少竞争并提高效率。

总结

区分从全局队列获取还是通过工作窃取获取任务主要依赖于调度器内部的逻辑流程和具体的实现细节。当 P 的本地队列为空时,调度器优先尝试从全局队列获取任务,因为这是最简单直接的方式。只有当全局队列也为空时,才会尝试从其他 P 窃取任务,这有助于保持负载均衡并充分利用系统资源。


知识点

  • 处理器P包含了运行Goroutine的资源,如果线程M想运行Goroutine,必须先获取P,P中还包含了可运行的G队列
  • 在Go中,线程是运行Goroutine的实体,调度器的功能是把可运行的Goroutine分配到工作线程上

[典藏版]深入理解Golang协程调度GPM模型

posted @ 2024-12-30 09:57  guanyubo  阅读(36)  评论(0编辑  收藏  举报