深入理解GMP

为什么需要调度器

  1. 线程/进程数量越多,切换成本越大
  2. 多线程 随着 同步竞争(如 锁。资源)
  3. 进程和线程内存占用大

什么是GMP

G:goroutine协程

P:processor处理器

M: thread内核级线程(machine)

调度器的设计策略

复用线程:避免频繁的创建、销毁线程,而是对线程的复用

1)work stealing机制

​ 当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程

  • 先从全局队列拿,全局没有的时候,在从其他线程绑定的P的本地队列“窃取”

  • 执行条件

    1、当前p本地队列有待执行g
    2、没有空闲的p和m, 全局g队列为空 (此时意味这全局繁忙)
    3、 需要处理网络 I/O

2)hand off机制

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

3)go func()调度过程

  1. go func()创建一个goroutine
  2. 有俩个存储G的本地,新建G优先放在P的本地队列,本地队列满就放在全局队列
  3. G指南运行在M,一个M必须有一个P,M与P是1:1的关系。P的本地为空,M会从(先全局后其他P的本地队列)拿一个G
  4. 一个M一个G的不断循环的过程
  5. 当M执行G的时候发生syscall或者阻塞,M会阻塞,当前一些G在执行,runtime就会吧M从P的删除,创建一个新的M去绑定P(P舍弃M,绑定新的M)
  6. M调用结束,拿一个新的G,拿不到,M进入休眠

GO的启动周期

M0和G0

M0:

  1. 启动程序后的编号为0的主线程
  2. 在全局变量runtime.m0中,不需要在heap上分配
  3. 负责执行初始化操作和启动第一个G
  4. 启动第一个G之后,M0和其他M同等

G0:

  1. 每次启动第一个M,都会第一个创建的gourtime,就是G0
  2. G0仅用于负责调度G
  3. G0不指向任何可执行函数G0就是来调度其他G,所有G得指挥所
  4. 每一个M都有一个自己的G0
  5. 在调度或者系统调用时候会使用M来切换到G0
  6. M0的G0在全局空间中

GMP可视化

trace调试

基本的trace:

  1. 创建trace文件 f, err:=os.Create(“trace.out”)
  2. 启动trace trace.Start()
  3. 停止trace trace.Stop()
  4. Go build 并且运行之后,会得到一个trace.out文件
  5. 通过go tool trace 工具打开trace文件,go tool trace trace.out

GMP终端GODEBUG调试

  • gomaxprocess P的数量 一般默认是和CPU核心数是一样的
  • idlleprocs 处理idle状态的P数量,gomaxprocs-idlleprocs= 目前正在执行的p的数量
  • threads 线程数量
  • spinningthreads 处于自旋的thread数量
  • idllethread 处理idle状态的thread数量
  • runqueue 全局G队列中的G的数量
  • 【0,0】每个P的local queue本地队列,目前存在G的数量
# 先go build xx.go 生成文件
# GODEBUG=schedtrace=1000 ./xx.go

GMP场景

场景1:G1创建G3

局部性:G3优先加入G1所在M(他们之间可能共享资源)【创建G优先在G所在M】

场景2:G1执行完毕

G1在完成退出之后,拿到G0,G0优先从本地队列P中取到其他G 运行

场景3、4、5:G2开辟过多G,全局队列满,全局队列不满

G2想要创建的G 超过了本地队列P的最大容量

G7,G8就是还要多出来创建的G

  1. 先存满本地队列P,满了把G7放入全局队列
  2. 平均分割本地队列P的一半
  3. 把前面一半G放入全局队列,同时如果本地队列满,不满创建G8

场景6:唤醒正在休眠的M

每次创建G,就会尝试唤醒M(前提有休眠队列中有空闲M)

  1. M寻找P绑定
  2. 本地队列P如果是空的,进入自旋寻找G

场景7:被唤醒的M从全局队列获取G

实现了全局队列到P的负载均衡

因为唤醒的M,此时本队队列P为空

n=min(len(GQ全局队列长度)/GOMAXPROCS+1,len(GQ)/2)

  1. 此时唤醒的M的本队P尝试从全局队列中获取n个G

场景8:M2从M1中偷取G

当M2中的本地队列P为空的时候,并且全局队列也为空

此时,分割一半M1的本地队列P,

M2的本地队列P”偷取”后一半的G到本地队列P

场景9:自旋线程的最大限制

GOMAXPROCS是P的数量限制

自旋线程+执行线程<=GOMAXPROCS

场景10:G发生系统调用/阻塞

  1. 阻塞|系统调用的G,直接绑定到M上
  2. 此时阻塞的M的P去寻找新的P,不然只能P进入到空闲队列P
  3. 找到新的M之后,P绑定到新的M5上

场景11:G发生非阻塞

G8恢复到非阻塞的状态,此时缺少P

  1. G8先寻找原配P,P2已绑定,就寻找其他空闲P
  2. G8找不到空闲P,就G8进入全局队列,M2进入休眠线程队列