Go--GMP调度模型

GMP模型

G、M、P简介

GMP是Go运行时调度层面的实现,包含4个重要结构,分别是G、M、P、Sched
在这里插入图片描述

  • G(Goroutine):代表Go 协程Goroutine,存储了 Goroutine 的执行栈信息、Goroutine 状态以及Goroutine 的任务函数等。G的数量无限制,理论上只受内存的影响,创建一个 G 的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个 Goroutine ,而且Go语言在 G 退出的时候还会把 G 清理之后放到 P本地或者全局的闲置列表 gFree 中以便复用。
  • M(Machine): Go 对操作系统线程(OS thread)的封装,可以看作操作系统内核线程,想要在 CPU 上执行代码必须有线程,通过系统调用 clone 创建。M在绑定有效的 P 后,进入一个调度循环,而调度循环的机制大致是从 P 的本地运行队列以及全局队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础。M的数量有限制,默认数量限制是 10000,可以通过 debug.SetMaxThreads() 方法进行设置,如果有M空闲,那么就会回收或者睡眠。
  • **P(Processor):虚拟处理器,M执行G所需要的资源和上下文,只有将 P 和 M 绑定,才能让 P 的 runq 中的 G 真正运行起来。P 的数量决定了系统内最大可并行的 G 的数量,**P的数量受本机的CPU核数影响,可通过环境变量$GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU核心数。
  • Sched:调度器结构,它维护有存储M和G的全局队列,以及调度器的一些状态信息

P和M的个数

  • P: 由启动时环境变量 $GOMAXPROCS 或者是由 runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。
  • M:
    • Go 语言本身的限制:Go 程序启动时,会设置 M 的最大数量,默认 10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略。
    • runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量。
    • 一个 M 阻塞了,会创建新的 M。

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

P和M何时会被创建

P: 在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

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

goroutine创建流程

在调用go func()的时候,会调用runtime.newproc来创建一个goroutine,这个goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址与程序计数器这些信息(备注:这些数据在goroutine被调度的时候会被用到。准确的说该goroutine在放弃cpu之后,下一次在重新获取cpu的时候,这些信息会被重新加载到cpu的寄存器中。)

创建好的这个goroutine会被放到它所对应的内核线程M所使用的上下文P中的run_queue中,等待调度器来决定何时取出该goroutine并执行,通常调度是按时间顺序被调度的,这个队列是一个先进先出的队列

goroutine什么时候会被挂起

  • waitReasonChanReceiveNilChan:对未初始化的 channel 进行读操作
  • waitReasonChanSendNilChan:对未初始化的 channel 进行写操作
  • 在 main goroutine 发生 panic 时,会触发
  • 在调用关键字 select 时会触发
  • 在调用关键字 select 时,若一个 case 都没有,会直接触发
  • 在 channel 进行读操作,会触发
  • 在 channel 进行写操作,会触发
  • sleep 行为,会触发
  • IO 阻塞等待时,例如:网络请求等
  • 在垃圾回收时,主要场景是 GC 标记终止和标记阶段时触发
  • GC 清扫阶段中的结束行为,会触发
  • 信号量处理结束时,会触发

GMP的调度

调度流程

在这里插入图片描述

  • 每个 P 有个局部队列,局部队列保存待执行的goroutine(流程 2),当 M 绑定的 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列(流 程 2-1)
  • 每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3), M 从绑定的 P 中的局部队列获取 G 来执行
  • 当 M 绑定的 P 的局部队列为空时,M 会从全局队列获取到本地队列来执行 G (流程 3.1),当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P 的局部队列中偷取 G 来执行(流程 3.2),这种从其他 P 偷的方式称为 work stealing
  • 当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M(流程 5.1)
  • 当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他 runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执 行(流程 5.3)

调度策略

由于 P 中的 G 分布在 runnext、本地队列、全局队列、网络轮询器中,则需要挨个判断是否有可执行的 G,大体逻辑如下:

  • 每执行61次调度循环,从全局队列获取G,若有则直接返回
  • 从P 上的 runnext 看一下是否有 G,若有则直接返回
  • 从P 上的 本地队列 看一下是否有 G,若有则直接返回
  • 上面都没查找到时,则去全局队列、网络轮询器查找或者从其他 P 中窃取,一直阻塞直到获取到一个可用的 G 为止

调度时机

什么时候进行调度(执行/切换)?

在以下情形下,会切换正在执行的goroutine

  • 抢占式调度
    • sysmon 检测到协程运行过久(比如sleep,死循环)
      • 切换到g0,进入调度循环
  • 主动调度
    • 新起一个协程和协程执行完毕
      • 触发调度循环
    • 主动调用runtime.Gosched()
      • 切换到g0,进入调度循环
    • 垃圾回收之后
      • stw之后,会重新选择g开始执行
  • 被动调度
    • 系统调用(比如文件IO)阻塞(同步)
      • 阻塞G和M,P与M分离,将P交给其它M绑定,其它M执行P的剩余G
    • 网络IO调用阻塞(异步)
      • 阻塞G,G移动到NetPoller,M执行P的剩余G
    • atomic/mutex/channel等阻塞(异步)
      • 阻塞G,G移动到channel的等待队列中,M执行P的剩余G

同时启动了一万个goroutine,会如何调度?

一万个G会按照P的设定个数,尽量平均地分配到每个P的本地队列中。如果所有本地队列都满了,那么剩余的G则会分配到GMP的全局队列上。接下来便开始执行GMP模型的调度策略:

  • 本地队列轮转:每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队首中重新取出一个G进行调度。
  • 系统调用:P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。当该G即将进入系统调用时,对应的M由于陷入系统调用而进被阻塞,将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。
  • 工作量窃取:多个P中维护的G队列有可能是不均衡的,当某个P已经将G全部执行完,然后去查询全局队列,全局队列中也没有新的G,而另一个M中队列中还有3很多G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值