稻田代码

GMP线程调度模型

GMP

gmp是golang的线程调度模型

含义

G - goroutine 协程(用户态线程)
M - thread 线程(内核态线程)
P - processor 处理器(CPU,CPU的内核数量决定了P的最大数量)

关系

G要绑定M才能运行,M要绑定P才能运行

同一时刻,P只能被一个M绑定(因为同一时刻万一多个M一起消费P的局部队列,则取局部队列也要一直加锁避免一个G被执行多次)
同一时刻,M只能被一个G绑定(因为M同一时刻只能处理一个G)

不同时刻,P可以被多个M绑定,M也可以被多个P绑定,如M1因G1阻塞的时候,P会绑定其他的M来处理剩余的G(多对多)
不同时刻,M可以被多个G绑定,G也可以被多个M绑定,如G1阻塞完成后如果没有空闲的P,则G1将进入全局队列等待被其他的M执行(多对多)

P:G 1对多
P:M 同一时刻1对1,不同时刻多对多
M:G 同一时刻1对1,不同时刻多对多

涉及名词

P列表:程序启动时创建,最多有 GOMAXPROCS(可配置) 个,go新版本默认使用最大内核数,比如电脑是8核cpu,则P的最大数量就是8
P的本地队列:每一个P都有一个本地队列来存放待运行的G
全局队列:系统用来存放等待运行的G
M休眠队列:系统用来存放空闲的M

调度策略

调度逻辑图

查看

GMP 整理调度策略

1. 程序启动时,先初始化和核数一样的P。 
2. 当执行go func()时,会创建一个G
3. G创建之后,系统会尝试获取一个可用的P并尝试把新的G放到这个P的本地队列中
   # 3.1-3.5 为work-stealing调度算法
   3.1 如果成功把G放到了某个P的本地队列中,此时P会从休眠的M队列中唤醒一个M并绑定来处理自己本地队列的G(如果M休眠队列中没有剩余的M了,就会创建一个新的M来绑定自己)
   3.2 一旦M绑定了P就会一直从p的本地队列中获取G来处理
   3.3 如果当前绑定的P的本地队列已经没有未处理的G了,这个M就尝试从全局队列中偷取一部分G(G/P个)来放到当前绑定的P的本地队列中,然后继续处理
   3.4 如果全局队列中也没有剩余的G了,M就从其他的P的本地队列中偷取一半的G放到当前绑定的P的本地队列中然后继续处理
   3.5 如果当前绑定P的局部队列、全局队列、其他P的本地队列都没有剩余待执行的G了,这个M就会进到休眠的M队列中(避免重新创建)
4. 如果所有的P的本地队列已满(没有可用的P),则会把这个G放到全局队列中

P 和 M 何时会被创建

  • P: 程序运行时提醒会创建GOMAXPROCS个P
  • M:当新的G放入某个P的局部队列之后,P会尝试唤醒空闲M队列的一个M来处理G,如果没有空闲的M,此时会新建一个M来处理G

优点

复用线程

  • work-stealing机制(任务偷取)
当M处理完当前绑定的P的本地队列,会从全局队列中偷取部分G来执行,
如果全局队列中也没有待执行的G了,就随机去其他的P的本地队列中偷取一半的G放到当前绑定的P的本地队列然后处理掉,
否则这个M将解绑P并进到休眠M队列中

充分利用了M,避免M被P频繁的绑定和解绑
  • hand off机制(任务移交)
当M1因为正在处理的G1进行了系统调用而阻塞时,M1会解绑当前的处理器P1并继续维护G1(M1和G1不解绑),P1会绑定新的线程M2继续处理剩下的G,相当于M1把P1剩下的G移交给了M2处理,这样保持了P继续剩下的G,充分利用了CPU。
当G1系统调用结束后,根据M1是否能获取到可用的P,将会将G1做不同的处理:
1. 如果有空闲的P(未被M绑定的P),则M1绑定一个空闲的P并继续执行G1(M需要绑定P才能运行)
2. 如果没有空闲的P,则G1放入全局队列等待被其他的P调度,然后M1进度休眠队列

保持P不空闲,充分利用CPU
 
系统调用:系统调用是用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用。
。而因为内核只有限的(8核等),而线程是有很多的,内核需要不停的切换(并发)来处理每个线程(只是因为切换的很快,所以我们以为所有的线程是在同时运行的。所以同一时刻一个cpu只能处理一个线程,而其他的线程需要排队,当线程很多的时候可能就会发生阻塞)

CPU并行

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

抢占

在Go中一个G最多占用CPU 10ms,防止其他G被饿死。

补充

用户态线程的好处

内核态线程切换时需要系统动态的分配资源(内存),用户态线程




本原创文章未经允许不得转载 | 当前页面:稻田代码 » GMP线程调度模型