GoLang之goroutine底层系列一(GMP)

文章目录

  • GoLang之go Hello Goroutine的执行过程底层GMP(一)
    • 1.println("Hello World!")
    • 2.go Hello(一个p)
    • 3. go Hello(多个p)
    • 4.总结
    • 4.附

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

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

1.println(“Hello World!”)

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

GoLang之goroutine底层系列一(GMP)_第1张图片

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

GoLang之goroutine底层系列一(GMP)_第2张图片

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

GoLang之goroutine底层系列一(GMP)_第3张图片

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

GoLang之goroutine底层系列一(GMP)_第4张图片

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

GoLang之goroutine底层系列一(GMP)_第5张图片

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

GoLang之goroutine底层系列一(GMP)_第6张图片

GoLang之goroutine底层系列一(GMP)_第7张图片

GoLang之goroutine底层系列一(GMP)_第8张图片

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

GoLang之goroutine底层系列一(GMP)_第9张图片

GoLang之goroutine底层系列一(GMP)_第10张图片

GoLang之goroutine底层系列一(GMP)_第11张图片

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

GoLang之goroutine底层系列一(GMP)_第12张图片

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

GoLang之goroutine底层系列一(GMP)_第13张图片

GoLang之goroutine底层系列一(GMP)_第14张图片

GoLang之goroutine底层系列一(GMP)_第15张图片

GoLang之goroutine底层系列一(GMP)_第16张图片

GoLang之goroutine底层系列一(GMP)_第17张图片

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

GoLang之goroutine底层系列一(GMP)_第18张图片

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

GoLang之goroutine底层系列一(GMP)_第19张图片

GoLang之goroutine底层系列一(GMP)_第20张图片

GoLang之goroutine底层系列一(GMP)_第21张图片

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

GoLang之goroutine底层系列一(GMP)_第22张图片

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

GoLang之goroutine底层系列一(GMP)_第23张图片

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

GoLang之goroutine底层系列一(GMP)_第24张图片

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

GoLang之goroutine底层系列一(GMP)_第25张图片

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

GoLang之goroutine底层系列一(GMP)_第26张图片

2.go Hello(一个p)

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

GoLang之goroutine底层系列一(GMP)_第27张图片

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

GoLang之goroutine底层系列一(GMP)_第28张图片

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

GoLang之goroutine底层系列一(GMP)_第29张图片

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

GoLang之goroutine底层系列一(GMP)_第30张图片

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

GoLang之goroutine底层系列一(GMP)_第31张图片

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

GoLang之goroutine底层系列一(GMP)_第32张图片

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

GoLang之goroutine底层系列一(GMP)_第33张图片
GoLang之goroutine底层系列一(GMP)_第34张图片

GoLang之goroutine底层系列一(GMP)_第35张图片

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

在这里插入图片描述

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

GoLang之goroutine底层系列一(GMP)_第36张图片

等到sleep的时间到达后,timer会把main goroutine重新置为_Grunnable状态,放回到runq中。
再然后,main gorouting被m0执行,main.main结束,exit得以调用,进程退出。
GoLang之goroutine底层系列一(GMP)_第37张图片

GoLang之goroutine底层系列一(GMP)_第38张图片

GoLang之goroutine底层系列一(GMP)_第39张图片

GoLang之goroutine底层系列一(GMP)_第40张图片

3. go Hello(多个p)

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

GoLang之goroutine底层系列一(GMP)_第41张图片

GoLang之goroutine底层系列一(GMP)_第42张图片

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

GoLang之goroutine底层系列一(GMP)_第43张图片

4.总结

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

GoLang之goroutine底层系列一(GMP)_第44张图片
GoLang之goroutine底层系列一(GMP)_第45张图片

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

GoLang之goroutine底层系列一(GMP)_第46张图片

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

GoLang之goroutine底层系列一(GMP)_第47张图片

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

GoLang之goroutine底层系列一(GMP)_第48张图片

4.附

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

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

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

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

GoLang之goroutine底层系列一(GMP)_第49张图片

GoLang之goroutine底层系列一(GMP)_第50张图片

你可能感兴趣的:(GoLang底层,golang,开发语言,后端)