GoLang之goroutine底层系列一(GMP)

GoLang之go Hello Goroutine的执行过程底层GMP(一)

本文章请结合GMP 原理与调度 · Go语言中文文档 (topgoer.com)进行阅读

1.println(“Hello World!”)

一个HelloWorld程序,编译后成为一个可执行文件,执行时,可执行文件被加载到内存。对于进程虚拟地址空间中的代码段来说,我们感兴趣的是程序执行入口,它并不是我们熟悉的main.main,不同平台下程序执行入口不同。

image-20220309163258478

在进行一系列检查与初始化等准备工作后,

在这里插入图片描述

会以runtime.main为执行入口,创建main goroutine,main goroutine执行起来以后,才会调用我们编写的main.main(是我们自己定义的main包里的那个函数)

image-20220309163356088

再来看数据段,这里有几个重要的全局变量不得不提:
我们知道Go语言中协程对应的数据结构是runtime.g,工作线程对应的数据结构是runtime.m;

在这里插入图片描述

而全局变量g0就是主协程对应的g,与其它协程有所不同,g0的协程栈是在主线程栈上分配的,全局变量m0就是主线程对应的m;
g0持有m0的指针,m0里也记录着g0的指针,而且一开始m0上执行的协程正是g0(指curg=&g0),m0和g0就这样联系了起来;
全局变量allgs记录所有的g,allm记录所有的m

image-20220309163810653

最初Go语言的调度模型里只有M和G。所以待执行的G一排排做,等在一个地方,每个M来这里获取一个G时都要加锁,多个M分担这多个G的执行任务,就会因频繁加锁解锁而发生等待,影响程序并发性能。

image-20220309164447241

image-20220309164505914

image-20220309164521891

所以后来在M和G以外又引入了P,P对应的数据结构是runtime.p,它有一个本地runq,这样只要把一个P关联到一个M上,这个M就可以从P这里直接获取待执行的G,不用每次都和众多M从一个全局队列中争抢任务了。

在这里插入图片描述

image-20220309164651070

image-20220309164712666

也就说是,虽然P有一个本地runq,但是依然有个全局runq。它保存在全局变量sched中,这个全局变量代表的是调度器,对应的数据结构是runtime.schedt,这里记录着所有空闲的m,空闲的p等等许多和调度相关的内容,其中就有一个全局的runq。

image-20220309164835188

如果P的本地队列已满,那么等待执行的G就会被放到这个全局队列里,而M会先从关联的P持有的本地runq中获取待执行的G,没有的话,再到调度器持有的全局队列里取,如果全局队列也没有了,就会去别的P那里“偷”一些G过来

image-20220309164912599

image-20220309164949984

image-20220309165011849

image-20220309165104339

image-20220309165134309

同G和M一样,也有一个全局变量allp用于保存所有的P

image-20220309165212313

在程序初始化过程中会进行调度器初始化,这时会按照GOMAXPROCS这个环境变量,决定创建多少个P,保存在全局变量allp中,并且把第一个P(allp[0])与m0关联起来,简单来说,G,M,P就是这样的合作关系。

在这里插入图片描述

image-20220309165257857

image-20220309165350669

现在就可以更清晰的理解这个经典的示意图了。

image-20220309165411882

在main goroutine创建之前,GPM的情况是这样的

在这里插入图片描述

main goroutine创建之后,新的G被加入到当前P的本地队列中。

在这里插入图片描述

然后通过mstart函数开启调度循环,这个mstart函数,是所有工作线程的入口,主要就是调用schedule函数,也就是执行调度循环,其实对于一个活跃的m而言,不是在执行某个G,就是在执行调度程序获取某个G,我们暂且不展开循环调度的具体逻辑,目前面临的调度场景很简单,队列里只有main goroutine等待执行

image-20220309170547276

所以m0切换到main goroutine,执行入口自然是runtime.main,它会做很多事,包括创建监控线程,进行包初始化等等,其中也包括调用我们熟悉的main.main函数,终于可以输出“Hello World!”了。值得一提的是,在main.main返回之后,runtime.main会调用exit()函数结束进程

image-20220309170750264

2.go Hello(一个p)

接下来我们把这个hello world程序改造一下,如果在main.main中不直接输出,而是通过一个协程输出,那么到main.main被调用执行时,就会创建一个新的goroutine,我们把它记为“hello goroutine”

在这里插入图片描述

我们通过go关键字创建协程,会被编译器转换为newproc函数调用

在这里插入图片描述

main goroutine也是由newproc函数创建的。

在这里插入图片描述

创建goroutine时我们只负责指定入口、参数,而newproc会给goroutine构造一个栈帧,目的是让协程任务结束后,返回到goexit函数中,进行协程资源回收处理等工作,这很合理,一个协程任务完成后,是放到空闲G队列里备用,还是该释放,总要有个出路

image-20220309175624919

回到newproc(0,hello),如果我们设置GOMAXPROCS=1,那么就只会创建一个P。那么新创建的hello goroutine会被添加到当前P的本地runq队列中,然后main.main就结束返回了,再然后exit函数被调用,进程就结束了,然后。。,就没有然后了,所以hello groutine它没能执行,问题就在于main.main返回后exit函数就会被调用,直接把进程结束掉,没给hello goroutine空出调度执行的时间。

在这里插入图片描述

所以想让hello goroutine执行,就要在main.main返回之前拖延下时间

image-20220309185556753

如果使用time.Sleep,实际上会调用gopark函数,把当前协程的状态从_Grunning修改为_Gwaiting。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

然后main goroutine不会返回到当前P的runq中,而是在timer中等待,

在这里插入图片描述

继而调用schedule()进行调度,hello goroutine得以执行。

在这里插入图片描述

等到sleep的时间到达后,timer会把main goroutine重新置为_Grunnable状态,放回到runq中。
再然后,main gorouting被m0执行,main.main结束,exit得以调用,进程退出。
在这里插入图片描述

在这里插入图片描述

image-20220309185854111

image-20220309185921851

3. go Hello(多个p)

以上是有一个P的情况,如果创建了多个P,hello gorouine创建之后,虽然默认会添加到当前P的本地队列中,但是在有空闲P的情况下,就可以启动新的线程关联到这个空闲的P。并把hello goroutine放到它的本地队列中了。

image-20220309190144045

image-20220309190208408

同样的,可以使用time.Sleep,或者是等待一个Channel,或者是WaitGroup,反正只要main.main不马上返回,hello goroutine就有时间得以执行了

image-20220309190247786

4.总结

这一次我们通过hello world程序了解了一个go程序启动的大致过程

在这里插入图片描述
在这里插入图片描述

认识了g0,m0等非常重要的全局变量

在这里插入图片描述

也初步了解了GPM三者的关系

在这里插入图片描述

接下来就学习协程创建,调度,以及监控线程等关键内容展开学习一下,逐步加深对go语言中GMP模型的理解

在这里插入图片描述

4.附

所有的函数都必须直接或者间接的被 main 函数调用才可以运行

Go 语言程序的运行,是从 main 函数开始的

Go 语言的 main 函数,是一个没有任何 参数返回值 的函数

main函数所在的文件main.go里的package必须得声明名字为“main”,否则运行不了

image-20220309180051063

image-20220309180527565

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值