Go调度场景

0 阅读8分钟

Go调度场景解析:

场景1:G1创建G2

体现了GPM调度的局限性.P1拥有G1.M1获取P1后开始运行G1. P2拥有G2.M2获取P2后开始运行G2如下图.

当G1使用go func()创建了G3.为了局部性.G3优先加入P1本地队列.如下图.

场景1主要体现了GPM调度器的局部性.默认规定.如果一个G1创建一个新的G3.则这个G3会优先放在G1所在的本地队列中.这是由于G1和G3所保存的内存和堆栈信息最为相同.它们目前所在的M1和P1对于G1和G3的切换成本非常小.这也是局部性要保证的特点.

场景2:G1执行完毕:

当前M1和G1绑定.并且P1本地队列中有G2.M2和G3绑定.并且本地P2队列中有G4.如图所示.

当G1执行完成后.M1上运行的Goroutine切换为G0.如图所示.

G0负责调度时协程的切换.从P1本地队列获取G2.从G0切换到G2.并开始运行G2.实现了线程M1的复用.

场景2主要体现了GPM调度模型对M的复用性.当一个G已经执行完毕.不管其他P中有没有空闲的.M一定会优先从自己绑定的P中的本地队列获取待调用的G来执行.

场景3:G2开辟过多的G

假设每个P的本地队列只能保存4个G.而此时的P1的本地队列是空的.若G1此时要创建6个G.则此时G1只能创建前四个G(G2 G3 G4 G5)放在G1的当前所在的队列中.多余的G将不会添加到P1的本地队列中.如下图.

场景3表示了一个G创建了过多的G.则本地队列会出现放满的现象.所以多余的G会按照场景4的逻辑进行安排.

场景4:G1本地满在创建G7

G1在创建G6的时候.发现P1的本地队列已满.需要执行负载均衡算法.把P1中本地队列中的前一半的G.还有新创建的G转移到全局队列.如图所示.

这些G转移到全局队列时.会被打乱顺序.

场景5:G1本地未满在创建G7

G1创建G7的时候.P1本地队列未满.所以G7会被加入到P1的本地队列.如图所示.

新创建的G会优先被放到本地队列中.也是由于局部性质导致.由于本地队列还有其他G在队列头部.所以新创建的G会依次从队列尾部进入.

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

在GPM模型中.在创建一个G的时候.运行的G会尝试唤醒其他空闲的P和M组合去执行.含义是.如果有可能之前有过剩的M.这些M不会立即不回收.而是会放在一个休眠线程队列.触发这个M从休眠队列唤醒的条件就是在尝试创建一个G的时候.

如果所示.目前只有一个P和M在绑定.并且M1正在执行G1.

当G1尝试创建一个新的G的时候.就会触发尝试从休眠线程队列获取M.并且尝试去绑定新的P以及P的本地队列.

M2如果发现有可被利用的P资源.则M2就会被激活.并且绑定到P2上.如图所示.

此时M2和P2若绑定.就需要寻找其他的G去执行.每个M都会有一个调度其他的G0.所以目前M2和P2在没有正常的G可用的时候.G0会被P调度.如果P的本地队列为空.并且P正在调度G0.则M2 P2和G0组合就被称为一个自旋线程.

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

M2尝试从全局队列(简称CQ)取一批G放到P2的本地队列(简称LQ).M2从全局队列取的G数量符合下面公式.

n = min( len(CQ) / (GOMAXPROCS + 1),cap(LQ) / 2 )

至少从全局队列取一个G.但每次不要从全局队列将太多的G移动到P本地队列.给其他P留一部分.这是从全局队列到P本地队列的负载均衡.

如果此时M2为自旋线程状态.全局队列的数量为3.并且P2的本地队列容量为4.则通过负载均衡公式得到.一次从全局队列获取G的个数为1,M2就会从全局的头部获取G4加入P2本地队列.如下图.

G3加入本地队列之后.就需要被G0调度.G0也就被替换为G4.并且与此同时全局队列中的G5和G6会依次像队列的头部移动.如下图所示.

一旦被调度起来.M2就不是自旋线程了.

场景8:M2从M1中偷取

假设G1一直在M1上运行.经过两轮后.M2已经把本地P任务和全局队列都执行完了.如下图.

M2又会处于调度G0的状态.此时的M2处于自旋线程状态.处于自旋状态的MP组合会不断地寻找可以调度的G.否则在这空等待就是在浪费线程资源.

全局队列已经没有G.所以M要执行偷取.从其他G的P那里偷取一半的G过来.放到自己的P本地队列.P2从P1本地队列尾部取一半的G.如图所示.

本例子中一半只有一个G.所以会被偷过来放到本地队列.如下图.

接下来的过程与之前场景相似.

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

M1本地队列任务都已经被M2偷走执行完成.如下图.

GMP模型中GOMAXPROCS变量用于确定P的数量.在GMP中P的数量是固定的.一旦确定了P的数量以后.P的数量就不可以动态添加或者删减了.

假如现在GOMAXPROCS=4.那么P的数量也是4.此时P3和P4绑定了M3和M4.如图所示.

M3和M4没有Goroutine也可以运行.所以目前绑定的都是各自的G0.处于自旋状态不断寻找Goroutine.

自旋的本质上是在运行.线程在运行却没有执行G.就变成了浪费CPU.为什么不销毁.因为创建和销毁CPU也会浪费时间.Go调度器GMP模型的思想是当有新的Goroutine创建时.立刻能有M运行它.如果销毁在创建就增加了时延.

多余的线程就会进入休眠.如下图.

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

假定当前除了M3和M4位自旋线程.还有M5和M6为空闲的线程(没有得到P的绑定.注意这里最多只能有4个P.所以P的数量应该永远是M>=P).G7创建了G8.如图所示.

若此时G7进行了阻塞的系统调用.则M2和P2立即解绑.P2会执行以下判断.如果P2本地队列有G或全局队列有G执行.并且有空闲的M.则P2会立即唤醒一个M和它绑定(如果没有空闲的M.则会创建一个M进行绑定).否则P2会加入空闲P队列.等待M获取可用的P.

P2本地队列有G8.可以和其他空闲线程M5绑定.如图所示.

场景11:G发生非阻塞的系统调用

接着场景10.调用阻塞结束.如下图.

场景10中M2和P2虽然会解绑.但M2会记住P2.然后G7和M2进入系统调用状态.当G7和M2退出系统调用.M2会尝试获取之前具有绑定关系的P2.如果P2可以被获取.则M2和P2重新绑定.如果无法获取M2,则会进行其他方式处理.

此时的P2和M5正在绑定.M2自然获取不到.M2自然会从空闲队列中获取P.如图所示.

如果依然没有空闲的P在队列中.则M2就等于找不到可用的P与自己进行绑定G7会被记为可运行状态.并加入全局队列.M2因为没有P的绑定而变成休眠状态(长时间等待会被GC回收销毁).

风丝袅,水浸碧天清晓。一镜湿云青未了,雨晴春草草。

梦里轻螺谁扫,帘外落花红小。独睡起来情悄悄,寄愁何处好。 纳兰

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.

如果喜欢我的分享的话.可以关注我的微信公众号

念何架构之路