Golang调度器GMP原理

Golang调度器由来

  • 单进程时代不需要调度器

    早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程

单进程操作系统面临问题:

1
2
1. 单一的执行流程,计算机只能一个任务一个任务处理
2. 进程阻塞所带来的CPU时间浪费
  • 多进程/线程

    多进程/多线程的操作系统中,解决了阻塞的问题,因为一个进程阻塞CPU可以立刻到其他进程中执行

多进程面临的问题:

1
2
3
4
1. 进程的创建、切换、销毁都会占用很长的CPU事件。
2. 进程过多,CPU很大部分被用来进行进程调度

大量的进程/线程会出现高内存占用、调度的高消耗CPU
  • 协程

(N:1关系)

1
N个协程绑定一个线程,协程在用户态线程即完成切换,不会陷入内核态,这种切换非常的轻量快速.

(M:N关系)

Go语言的协程 goroutine

Go使用groutinuechannel,来提供更容易使用的并发.

Goroutine特点:

1
2
1. 占用内存更小(几kb)
2. 调度更灵活(runtime调度)

G: 标识Groutinue M: 标识thread线程

Goroutine调度器 GMP模型

G: 标识Groutinue M: 标识thread线程, P: Processor

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

GPM

1
2
3
4
1. 全局队列(Global Queue): 存放等待运行的Groutinue
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,不断重复下去

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

  • 有关P和M的个数问题

P的数量:

1
由环境变量$GOMAXPROCS或者runtime.GOMAXPROCS()决定, runtime.GOMAXPROCS(0) 表示最大可用的CPU数量,这意味着程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行.

M的数量:

1
2
3
Go语言本身限制: Go程序启动,会设置M的最大数量,默认10000
runtime/debug debug.SetMaxThreads has a default limit of 10k threads, which is enforced by runtime.checkcount when the M is created.
一个M阻塞,会创建新的M

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来.

  • P和M何时会被创建
1
2
1. P何时创建: 在确定P的最大数量n后,运行时系统会根据这个数量创建nP
2. M何时创建: 没有足够的M来关联P运行其中可运行的G时,比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的就去创建新的M
  • 调度器的设计策略
    复用线程: 避免频繁的创建,销毁线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1. Work Stealing机制
    当本线程无可运行的G时,尝试从其他线程绑定的P中偷取G,而不是销毁线程

    2. Hand off 机制
    当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行

    2.1: 利用并行,GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行.
    2.2: 抢占: 在Goroutinue最多占用CPU10ms,防止其他Goroutinue饿死,
    2.3: 全局G队列: 当M执行work stealing从其他P偷取不到G时,可以从全局G队列获取G
  • go func() 调度流程

  • 特殊的M0和G0

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

G0: 每次启动M都会第一个创建的goroutine,G0用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个字节的G0

参考资料

Concurrency is not parallelism


Golang调度器GMP原理
https://blog.chyidl.com/2024/02/20/go调度器/
作者
Yaqing Chyi
发布于
2024年2月20日
许可协议