go语言学习笔记(三):调度器基础-走近那座山
走向Go调度器的基本原理本文总结了12个基本的场景覆盖了以下基本内容G的创建和分配。P的本地队列和全局队列的负载均衡。M如何寻找G。M如何从G1切换到G2。work stealingM如何去偷G。为何需要自旋线程。G进行系统调用如何保证P的其他G’可以被执行而不是饿死。Go调度器的抢占。场景1P0拥有G2M0获取P0后开始运行G2G2使用go func()创建了G3为了局部性G3优先加入到P0的本地队列。G2运行完成后(函数goexit)M0上运行的goroutine切换为G0G0负责调度时协程的切换函数schedule。从P0的本地队列取G3从G0切换到G3并开始运行G3(函数execute)。实现了线程M0的复用。场景2假设每个P的本地队列只能存4个G。G2要创建了6个G前4个GG3, G4, G5, G6已经加入P0的本地队列P0本地队列满了。G2在创建G7的时候发现P0的本地队列已满需要执行负载均衡把P0中本地队列中前一半的G还有新创建的G转移到全局队列实现中并不一定是新的G如果G是G2之后就执行的会被保存在本地队列利用某个老的G替换新G加入全局队列这些G被转移到全局队列时会被打乱顺序。所以G3,G4,G7被转移到全局队列。场景3G2创建G8时P0的本地队列未满所以G8会被加入到P0的本地队列。场景4在创建G时运行的G会尝试唤醒其他空闲的P和M执行。假定G2唤醒了M1M1绑定了p1并运行G0但P1本地队列没有GM1此时为自旋线程。M1尝试从全局队列取一批G放到P1的本地队列。M1从全局队列取G数量符合下面的公式1n min(len(GQ)/GOMAXPROCS 1, len(GQ/2))公式的含义是至少从全局队列取1个G但每次不要从全局队列移动太多的G到P本地队列给其他P留点。这是从全局队列到P本地队列的负载均衡。假定我们场景中一共有3个P所以M1只从能从全局队列取1个G即G3移到P1本地队列然后完成从G0到G3的切换运行G3。场景5假设G2一直在M0上运行经过2轮后M1已经把G7、G4也挪到了P1的本地队列并完成运行全局队列和P1的本地队列都空了如上图左边。全局队列已经没有G那M1就要执行work stealing从其他有G的P那里偷取一半G过来放到自己的P本地队列。P1从P0的本地队列尾部取一半的G本例中一半则只有1个G8放到P1的本地队列情况如上图右边。场景6P0本地队列G5、G6已经被其他M偷走并运行完成当前M0和M1分别在运行G2和G8M2和M3没有G可以运行M2和M3处于自旋状态它们不断寻找G。为什么要让M2和M3自旋自旋本质是在运行线程在运行却没有执行G是否浪费CPU销毁线程不是更好吗可以节约CPU资源。创建和销毁线程都是浪费CPU时间的我们希望当有新G创建时立刻能有M运行它如果销毁再新建就增加了时延降低了效率。当然也考虑了过多的自旋线程是浪费CPU所以系统中最多有GOMAXPROCS个自旋的线程多余的没事做线程会让他们休眠见函数notesleep()。场景7假定当前除了m3和m4为自旋线程还有m5和m6为自旋线程g8创建了g9g8进行了阻塞的系统调用m2和p2立即解绑p2会执行以下判断如果p2本地队列有g、全局队列有g或有空闲的mp2都会立马唤醒1个m和它绑定否则p2则会加入到空闲P列表等待m来获取可用的p。本场景中p2本地队列有g可以和其他自旋线程m5绑定。场景8g8创建了g9假如g8进行了非阻塞系统调用CGO会是这种方式见cgocall()m2和p2会解绑但m2会记住p然后g8和m2进入系统调用状态。当g8和m2退出系统调用时会尝试获取p2如果无法获取则获取空闲的p如果依然没有g8会被记为可运行状态并加入到全局队列。场景9Go调度在go1.12实现了抢占应该更精确的称为请求式抢占那是因为go调度器的抢占和OS的线程抢占比起来很柔和不暴力不会说线程时间片到了或者更高优先级的任务到了执行抢占调度。go的抢占调度柔和到只给goroutine发送1个抢占请求至于goroutine何时停下来那就管不到了。抢占请求需要满足2个条件中的1个1G进行系统调用超过20us2G运行超过10ms。调度器在启动的时候会启动一个单独的线程sysmon它负责所有的监控工作其中1项就是抢占发现满足抢占条件的G时就发出抢占请求。场景融合如果把上面所有的场景都融合起来就能构成下面这幅图了它从整体的角度描述了Go调度器各部分的关系。图的上半部分是G的创建、负载均衡和work stealing下半部分是M不停寻找和执行G的迭代过程。