Go调度器设计思想

Go调度器设计思想[GMP调度模型]

线程和协程

线程由CPU调度,是抢占式的。

协程由用户态调度,是协作式的,一个协程让出CPU后,才能执行下一个协程。

CSP

Communicating Sequnetial Process,通讯顺讯进程,是七大并发模型中的一种。

核心观念是将两个并发执行的实体,通过通道channel连接起来,所有的消息都通过channel传输。

Go语言对CSP并发模型的实现-GMP调度模型。

通过通信的方式共享内存。

不通过共享内存进行通信,而是通过通信实现共享内存。

GMP概念[GMP调度模型]

GMP代表了三个角色:Goroutine、Processor、Machine。

  • G (Goroutine)

用go关键字创建的执行体,对应一个结构体g,结构体里保存了goroutine的堆栈信息。

  • Machine

表示操作系统线程。

  • Processor

表示处理器、通过Process建立G、M联系。

G(Goroutine)

Goroutine就是代码中使用go关键字创建的执行单元,也是就是协程。

协程是不为操作系统所知的,由编程语言层面实现,上下文切换。

M(Machine)

goroutine调度器与os调度器通过M结合起来,每个M代表一个内核线程。

os调度器负责把内核线程,分配到CPU的核心上执行。

线程想要运行任务就得获取P。

从P的本地队列获取G,P本地队列为空时,M尝试从全局队列拿一批G放到P的本地队列。或者从其他P的本地队列偷一半,放到自己P的本地队列。

M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

没有足够的M来关联P并运行其中的可运行G,比如所有的M此时都阻塞住了,而P中还有很多就绪的任务,就会寻找空闲的M,没有空闲的,就会去创建新的M。

  • 简单调度机制过程:
    1. M绑定P进入调度循环[M要先获得P才能进度调度循环]
    2. 从P的队列(本地/全局)获取G
    3. 切换到G的执行栈执行G函数
    4. 调用goexit做清理工作
    5. 回到M

P(Process)

P(Process),调度器,虚拟处理器。

processor,包含了运行goroutine的资源,如果线程想要运行goroutine,必须先获取P,P中还包含了可运行的G队列。

线程是运行goroutine的实体,调度器的功能,是把可运行的goroutine分配到工作线程上。

P的数量决定了系统最大可并行的G的数量,P的数量,受本机CPU核数的影响。默认是CPU的核心数。

M和P的数量没有绝对关联关系,一个M阻塞,P就会去创建或者切换另一个M。

全局队列

存放等待运行的G。

P的本地队列

同全局队列类型,存放的也是等待运行的G。

存放数量有限,不超过256个。

新建G时,G加入到本地P队列,如果队列满了,把本地队列中,一半的G,移动到全局队列。[新的G实在本地队列还是全局队列?]

P列表

所有的P都在程序启动时创建,并且保存在数组中,最多有GOMAXPROCS个,在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。

调度器设计

早期不支持抢占式调度,这导致一旦某个G中出现死循环的代码逻辑,那么这个G将永久的占用分配给它的P和M,而同一个P中的G将得不到调度,出现饿死的情况。

在Go1.2版本,实现了基于协作的抢占式调度。

在Go1.4版本,实现了基于信号的抢占式调度。

设计思想

  • 抢占调度

一个goroutine最多占用CPU10ms,防止其他goroutine被饿死。

  • 线程复用

work stealing 机制 和 hand off 机制。

避免频繁的创建、销毁线程。对线程进行复用。

  • 利用并行

设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。

G调度流程

创建G:go func()

保存G:新创建的G会先保存在P的本地队列中,如果本地队列已满,则保存到全局队列中。

唤醒或新建M,绑定P,用于执行G:G只能运行在M中,一个M必须持有一个P,在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。

M获取G:

  1. 首先从P的本地队列获取G;
  2. 如果P为空,则从全局队列获取G;
  3. 如果全局队列也为空,则从另一个本地队列偷取一半数量的G(负载均衡),这种从其他P偷的方式,称为work stealing。

M调度G执行:

  1. 在执行G的过程发生系统调度阻塞(同步),会阻塞G和M(操作系统限制),此时P会和当前M解绑,并寻找新的M,如果没有空闲的M就会新建一个M,接着继续执行P中其余的G,这种阻塞后释放P的方式称为hand off。
  2. 系统调度结束后,这个G会尝试获取一个空闲的P执行,优先获取之前绑定的P,并放到这个P的本地队列,如果获取不到P,这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入到全局队列中。
  3. 如果M在执行G的过程中,发生网络IO的操作阻塞(异步),阻塞G,不会阻塞M,M会寻找P中其他可以执行的G继续执行,G会由网络轮训器(network poller)接手,当阻塞的G恢复后,G从network poller被移回到P的LRQ(Local Run Queue,本地运行队列)中,重新进入可执行状态。异步情况下,通过调度,Go Scheduler成功地将I/O任务转变成CPU任务,或者说将内核级别的线程切换转变成了用户级的goroutine切换,提高了效率。
  4. M执行完G后清理线程,重新进入调度循环(将M上运行的goroutine切换为G0),G0负责调度时协程的切换。

调度器生命周期 G0、M0

M0是启动程序后编号为0的主线程,这个M对应的实例会在全局变量runtime.M0中,不需要再heap上分配,M0负责执行初始化操作和启动第一个G,在这之后M0就和其他的M一样了。

G0是每次启动一个M都会第一个创建的goroutine,G0仅负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间。全局变量的G0是M0的G0。G0是用来做调度的,如:从G1切换到G2时,会先切回G0,保存G1的栈等调度信息,然后再切换到G2。

package main
import "fmt"
func main() {
    fmt.Println("Hello World")
}
  • 以上代码的运行流程:
  1. runtime创建最初的线程M0和G0,并把两者关联;
  2. 调度器初始化:初始化M0、栈、垃圾回收,创建和初始化由GOMAXPROCS个P构成的P列表。
  3. 实例代码中的main函数main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine,然后main goroutine加入到P的本地队列。
  4. 启动M0,M0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
  5. G拥有栈,M根据G中栈信息和调度信息设置运行环境。
  6. M运行G;
  7. G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行defer和panic处理,或调用runtime.exit退出程序。

调度器的生命周期,几乎沾满一个G程序的一生。runtime.main和goroutine执行之前都是为了调度器做准备工作,runtime.main和goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束。

抢占式调度

  • 基于协作的抢占式调度
    • Go1.2中实现了基于协作的抢占式调度,协作式:是否让出P的决定权在goroutine自身。
      1. 编译器会在调用函数前插入 runtime.morestack,让运行时有机会在这段代码中检查是否需要执行抢占调度;
      2. Go语言运行时会在垃圾回收站定程序,系统监控发现goroutine运行超过10ms,那么会在这个协程设置一个抢占标记。
      3. 当发生函数调用时,可结合编译器插入的runtime.morestack,调用runtime.netstack会检查抢占标记,如果有抢占标记就会触发抢占,让出CPU,切换调度主协程。

这种方案只局部解决饿死问题,只在有函数调用的地方,才能插入抢占代码(埋点),对于没有函数调用而是纯算法循环计算的G,Go调度器依然无法抢占。

  • 基于信号的抢占式调度
    • Go1.4中实现了基于信号的抢占式调度,不管协程有没有意愿主动让出CPU运行权,只要某个协程执行时间过长,就会发送信号,强行夺取CPU的运行权。
      1. M注册一个SIGURG信号的处理函数:sighandler;
      2. sysmon启动后,会间隔性的进行监控,最长间隔10ms,最短间隔20us,如果发现某协程独占P超过10ms,会给M发送抢占信号。
      3. M接收到信号后,内核执行sighandler函数把当前协程的状态从_Grunning正在执行改为_Grunnable可执行,把抢占的写成放到全局队列里,M继续寻找其他goroutine来运行。
      4. 被抢占的G再次调度过来执行时,会继续原来的执行流。

抢占分为_Prunning和_Psyscall:

_Psyscall抢占通常是有阻塞性系统调用的,比如磁盘IO,CGO。

_Prunning抢占通常是有一些类似死循环的计算逻辑引起的。

posted @ 2025-08-06 22:30  biby  阅读(11)  评论(0)    收藏  举报