golang 调度器调度场景

本文详细介绍了Go语言调度器的工作原理,包括Goroutine的创建、执行、负载均衡、线程自旋以及系统调用等场景。通过实例解析展示了调度器如何高效地管理和调度Goroutine,确保并发执行的高效性和线程资源的有效利用。
摘要由CSDN通过智能技术生成

golang 调度器调度场景



场景1 创建G


P拥有G1,M1获取P后开始运行G1,G1使用go func()创建了G2,为了局部性G2优先加入到P1的本地队列。


请添加图片描述

场景2 G执行完毕


G1运行完成后(函数:goexit),M上运行的goroutine切换为G0,G0负责调度时协程的切换(函数:schedule)。从P的本地队列取G2,从G0切换到G2,并开始运行G2(函数:execute)。实现了线程M1的复用。


请添加图片描述

场景3 G2开辟过多的G


假设每个P的本地队列只能存3个G。G2要创建了6个G,前3个G(G3, G4, G5)已经加入p1的本地队列,p1本地队列满了。接着触发场景4和5。


请添加图片描述

场景4 G2本地满再创建G7


G2在创建G7的时候,发现P1的本地队列已满,需要执行负载均衡(把P1中本地队列中前一半的G,还有新创建G转移到全局队列)

(实现中并不一定是新的G,如果G是G2之后就执行的,会被保存在本地队列,利用某个老的G替换新G加入全局队列)


请添加图片描述

这些G被转移到全局队列时,会被打乱顺序。所以G3,G4,G7被转移到全局队列。


场景5 G2本地未满创建G8


G2创建G8时,P1的本地队列未满,所以G8会被加入到P1的本地队列。


请添加图片描述

G8加入到P1点本地队列的原因还是因为P1此时在与M1绑定,而G2此时是M1在执行。所以G2创建的新的G会优先放置到自己的M绑定的P上。


场景6 唤醒正在休眠的M


规定:在创建G时,运行的G会尝试唤醒其他空闲的P和M组合去执行


请添加图片描述

假定G2唤醒了M2,M2绑定了P2,并运行G0,但P2本地队列没有G,M2此时为自旋线程**(没有G但为运行状态的线程,不断寻找G)**。


场景7 被唤醒的M从全局取C


M2尝试从全局队列(简称“GQ”)取一批G放到P2的本地队列(函数:findrunnable())。M2从全局队列取的G数量符合下面的公式:

n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))

至少从全局队列取1个g,但每次不要从全局队列移动太多的g到p本地队列,给其他p留点。这是从全局队列到P本地队列的负载均衡


请添加图片描述

假定我们场景中一共有4个P(GOMAXPROCS设置为4,那么我们允许最多就能用4个P来供M使用)。所以M2只从能从全局队列取1个G(即G3)移动P2本地队列,然后完成从G0到G3的切换,运行G3。


场景8 偷取G情况


假设G2一直在M1上运行,经过2轮后,M2已经把G7、G4从全局队列获取到了P2的本地队列并完成运行,全局队列和P2的本地队列都空了,如场景8图的左半部分。


请添加图片描述

全局队列已经没有G,那m就要执行work stealing(偷取):从其他有G的P哪里偷取一半G过来,放到自己的P本地队列。P2从P1的本地队列尾部取一半的G,本例中一半则只有1个G8,放到P2的本地队列并执行。


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


G1本地队列G5、G6已经被其他M偷走并运行完成,当前M1和M2分别在运行G2和G8,M3和M4没有goroutine可以运行,M3和M4处于自旋状态,它们不断寻找goroutine。


请添加图片描述

为什么要让m3和m4自旋,自旋本质是在运行,线程在运行却没有执行G,就变成了浪费CPU. 为什么不销毁现场,来节约CPU资源。因为创建和销毁CPU也会浪费时间,我们希望当有新goroutine创建时,立刻能有M运行它,如果销毁再新建就增加了时延,降低了效率。当然也考虑了过多的自旋线程是浪费CPU,所以系统中最多有自旋线程 + 执行线程 <= GOMAXPROCS个线程(当前例子中的GOMAXPROCS=4,所以一共4个P,2个自旋线程),多余的没事做线程会让他们休眠。


场景10 G发生调用阻塞


假定当前除了M3和M4为自旋线程,还有M5和M6为空闲的线程(没有得到P的绑定,注意我们这里最多就只能够存在4个P,所以P的数量应该永远是M>=P, 大部分都是M在抢占需要运行的P),G8创建了G9,G8进行了阻塞的系统调用,M2和P2立即解绑,P2会执行以下判断:如果P2本地队列有G、全局队列有G或有空闲的M,P2都会立马唤醒1个M和它绑定,否则P2则会加入到空闲P列表,等待M来获取可用的p。本场景中,P2本地队列有G9,可以和其他空闲的线程M5绑定。


请添加图片描述

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


G8创建了G9,假如G8进行了非阻塞系统调用,就是当G8的阻塞结束的时候。


请添加图片描述

M2和P2会解绑,但M2会记住P2,然后G8和M2进入系统调用状态。当G8和M2退出系统调用时,会尝试获取P2,如果无法获取,则获取空闲的P,如果依然没有,G8会被记为可运行状态,并加入到全局队列,M2因为没有P的绑定而变成休眠状态(长时间休眠等待GC回收销毁)。


小结


总结,Go调度器很轻量也很简单,足以撑起goroutine的调度工作,并且让Go具有了原生(强大)并发的能力。Go调度本质是把大量的goroutine分配到少量线程上去执行,并利用多核并行,实现更强大的并发。


本文转载于:

https://github.com/aceld/golang/blob/main/2%E3%80%81Golang%E7%9A%84%E5%8D%8F%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%99%A8%E5%8E%9F%E7%90%86%E5%8F%8AGMP%E8%AE%BE%E8%AE%A1%E6%80%9D%E6%83%B3%EF%BC%9F.md

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值