Go 2025-10-14 · 15 min 阅读

GMP调度


Go语言GMP介绍

Go语言相比Java,有更好的并发能力(GMP模型),同时其占用的服务器资源也较少,了解一下GMP的理念。从操作系统层面来看,线程是指内核级线程,是操作系统最小调度单元,创建、销毁、调度交由内核完成,可充分利用多核。协程(用户线程)与线程存在M:1的映射关系,从属于同一个内存级线程,无法并行,并且,一个协程阻塞会导致从属同一线程的所有协程无法执行。

Goroutine

经Golang优化后的协程,其有如下特点:1)与线程存在映射关系,为M:N;2)创建、销毁、调度在用户态完成,对内核透明,足够轻便;3)可利用多个线程,实现并行;4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;5)栈空间大小可动态扩缩,因地制宜;

在/runtime/proc.go的代码注释中,有对GMP的解释,其核心数据结构在/runtime/runtime2.go:

调度器设计策略

  1. 复用线程,避免频繁的创建、销毁线程,而是对线程的复用。work-stealing机制,当本线程没有可运行的G时,尝试从其它线程绑定的P中偷取G,而不是销毁线程。hand off机制,当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其它空闲的线程执行。
  2. 利用并行,GOMAXPROCS设置p的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。
  3. 抢占,在coroutine中要等待一个协程主动让出CPU才执行下一个协程(传统方式)。在Go中,一个goroutine最多占用cpu 10ms,防止其他goroutine被饿死。
  4. 全局G队列,当M执行work stealing从其它p偷不到g时,它可以从全局G队列中获取G。

go func()执行经历了什么?

  1. 我们通过一个go func()来创建一个goroutine;
  2. 有两个存储G的队列,一个是局部调用P的本地队列,一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了,就会保存在全局的队列中。
  3. G只能运行在M中,一个M必须持有一个P,M与P的关系是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P本地队列为空,就会想从其他的MP组合偷取一个可执行的G来执行。
  4. 一个M调度G执行的过程是一个循环机制;
  5. 当M执行某一个G时候如果发生了syscall或其它阻塞行为,M会阻塞,如果当前有一些G在执行,runtime就会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。
  6. 当M系统调用结束时,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中。

调度器的声明周期:

gmp调度场景过程全分析

  1. 场景一:P拥有G1,M1获取P后开始运行G1,G1使用go func()创建G2,为了局部性G2优先加入到P1的本地队列。
  1. 场景二:G1运行完成后(函数: goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数: schedule)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute),实现了线程M1的复用。
  1. 场景三、四、五,描述,如果G2连续创建6个G,则优先会将本地队列4个任务填满。当G2创建G7,则会将本地队列的一半的G打乱,和新创建的一块放在全局队列中。如果此时G2本地队列未满,则直接将G8加入本地队列。
  1. 场景六,唤醒正在休眠的M,规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行。假定G2唤醒了M2, M2绑定了P2,并运行了G0。但P2本地队列没有G,M2此时为自旋线程(没有G但为运行状态的线程,不断寻找G)。
  1. 场景七,被唤醒的M2从全局队列取批量G,M2尝试从全局队列(简称”GQ”)取一批G放到P2本地队列(函数: findrunnable()),M2从全局队列取的G数量符合下面的公式。
  1. 场景八,偷取G的情况,全局队列中已经没有G,那么m就要执行work stealing(偷取):从其他有G的P那里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,放到本地队列中执行。
  1. 场景九,最多有GOMAXPROCS个自旋的线程(当前例子中的GOMAXPROCS=4),多余的没事做的线程会让他们休眠。

  2. 场景十,G发生系统调用/阻塞,假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多只能够存在4个P,所以P的数量应该永远是M>=P),G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行如下判断:

    • 如果P2本地队列有G,全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定;
    • 否则P2则会加入到空闲P列表,等待M来获取可用的p,本场景中,P2本地队列有G9,还可以和其它空闲的线程M5绑定。
  1. 场景十一,G发生系统调用/非阻塞,描述,M2和P2会解绑,但M2会记住P2,然后G8和M2进入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列。M2因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。

GMP调度,源码分析

g0是一种特殊的调度协程,不执行用户函数,负责执行g之间的切换调度,与m的关系为1:1。goroutine的类型可分为两类:

当g0找到可执行g时,会调用gogo方法,调度g执行用户定义的任务。当g需要主动让渡时,会触发mcall方法,将执行权限重新交给g0;

广义”调度”可分为几种类型:

值得一提的是,前3种调度方式都由m下的g0完成。而抢占调用则是由一个全局监控协程monitor g来监控,倘若发现满足抢占调度的条件,则会从第三方的角度出手干预,主动发起该动作。从宏观上:

p每执行61次,会从全局队列中获取一个goroutine进行执行,同时会额外将全局队列中的一个goroutine放到本地队列中。若本地队列已满,则会返回来将本地队列中一半的g放回全局队列中,帮助当前p缓解执行压力;

扩展阅读,Go GC系列文章:

# Go