当前位置: 首页 > 科技观察

Go调度器:M、P和G

时间:2023-03-12 03:26:30 科技观察

这是另一篇关于Go调度器的文章。原文:GOSCHEDULER:Uber工程师Povilas的MS、PS和GS。网上已经有很多关于Go调度器的文章,比如Golang调度器源码分析。多读书加深记忆。大家也可以对比一下文章中有没有不准确的地方,更全面的了解Go调度器。我决定深入了解Go的内部结构,因为很长时间没有人写过有关Go调度程序的文章,我认为这是一个有趣的知识,所以让我们开始吧。基础知识Go的运行时管理调度、垃圾收集和goroutine运行时环境。本文只关注调度器。运行时负责运行goroutines并将它们映射到操作系统线程。Goroutines比线程更轻,而且启动成本低。每个goroutine都用一个G结构体表示,这个结构体的字段用来跟踪goroutine的堆栈(stack)和状态,所以可以认为G=goroutine。运行时管理G并将它们映射到逻辑处理器(称之为P)。P可以被视为需要获取的抽象资源或上下文,以便操作系统线程(称为M)可以运行G。可以获取多少P可以通过runtime.GOMAXPROCS(numLogicalProcessors)来控制。如果你需要调整这个参数(大多数情况下你不需要),只设置一次,因为它需要STWgc暂停。本质上,操作系统运行线程,线程运行您的代码。Go的诀窍在于编译器会在Go运行的时候在一些地方插入系统调用(比如通过通道发送值,调用运行时包等),这样Go就可以通知调度器执行特定的操作。上图的理解来自分析Go运行时调度器M,P和G之间的交互M,P和G之间的交互有点复杂。看下图高超的goruntimeschedulerslideshow:可以看到Go运行时有两种队列:一种是全局队列(在schedt结构中,很少用到),一种是每个P维护自己的G队列。为了运行goroutines,M需要持有上下文P。M将从P的队列中弹出一个goroutine并执行它。当你创建一个新的goroutine(gofunc()方法)时,它会被放入P的队列中。当然还有work-stealing调度算法。当M执行某个G时,如果它的队列为空,它会随机选择另一个P,从它的队列中取出一半的G在自己的队列中执行。.(偷!)当你的goroutine执行一个阻塞的系统调用(syscall)时,阻塞的系统调用会被中断(拦截),如果当前有一些G正在执行,runtime会把这个线程从P中移除(detach),然后创建一个新的操作系统线程(如果没有可用的空闲线程)为这个P服务。当系统调用继续时,这个goroutine被放入本地运行队列,线程将自己停放(休眠),并加入空闲线程。如果goroutine执行网络调用,运行时会做类似的事情。调用会被中断,但由于Go使用集成的网络轮询器,它有自己的线程,所以把它还给它。当以下goroutine被阻塞时,Go运行时将运行另一个goroutine:阻塞系统调用(例如打开文件)、网络输入、通道操作、同步包中的原语。schedulertracedebugGo可以跟踪runtimeScheduler,通过GODEBUG环境变量实现:$GODEBUG=scheddetail=1,schedtrace=1000./program以下是输出示例:SCHED0ms:gomaxprocs=8idleprocs=7threads=2spinningthreads=0idlethreads=0runqueue=0gcwaiting=0nmidlelocked=0stopwait=0sysmonwait=0P0:status=1schedtick=0syscalltick=0m=0runqsize=0gfreecnt=0P1:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0P2:status=0schedschedtick=0m=-1runqsize=0gfreecnt=0P3:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0P4:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0P5:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0P6:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0P7:status=0schedtick=0syscalltick=0m=-1runqsize=0gfreecnt=0M1:p=-1curg=-1mallocing=0throwing=0preemptoff=locks=1dying=0helpgc=0spinning=falseblocked=falselockedg=-1M0:p=0curg=1mallocing=0throwing=0preemptoff=locks=1dying=0helpgc=0spinning=falseblocked=falselockedg=1G1:status=8()m=0lockedm=0注意输出使用了G,M的概念而P以及它们的状态,比如P的队列大小如果不想关心这些细节,可以使用:$GODEBUG=schedtrace=1000./programWilliamKennedy写了一篇很好的文章解释这些细节。当然还有go自带的工具gotooltrace,它有一个UI可以让你查看你的程序和它的运行状态。你可以阅读这篇文章:Pusher。