前言
最近在尝试深入了解go的协程调度,专门按照自己的思路写一篇小博客记录一下,方便以后自己复习。
首先给大家推荐一篇写的超级棒的博客:深入golang runtime的调度. 其实这篇博客已经把goroutine的协程调度写的相当详细了,本文只是以自己习惯的理解流程去讲解一个go应用程序从程序启动时,到整个go程序跑起来的一个流程。是对阅读深入golang runtime的调度此文章的一个总结,如有理解的不对的地方,还望指出,谢谢!
主要源码文件
runtime/asm_amd64.s 进程启动时调用的一些汇编函数,可以理解为进程的入口。runtime/runtime2.go 主要是g、m、p、schedt的数据结构和g、p的状态定义,还定义了一些全局变量。runtime/proc.go 主要是调度器逻辑代码的实现
主流程
- asm_amd64.s创建m0, g0(此处的g0特指与m0对应的g0)
- asm_amd64.s创建用于运行runtime.main的g1(g1这个名字是我自创的)
- asm_amd64.s调用runtime.mstart启动m0(mstart是mstart1的一个wraper)
- runtime.mstart1调用schedule进入m0的调度循环
- 进入m0的调度循环后,调度器取出g1,而g1的任务函数为runtime.main,运行之
- runtime.main做了一系列操作之后(具体操作可以看我前言里推荐的博客),运行main_main(即我们程序代码中的main程序)
大致启动流程到此就结束了。
我估计很多人看到这里会跟我刚开始看深入golang runtime的调度这篇文章的时候一样懵。“这咋就结束了呢?用户自己创建的goroutine是如何运行的?又是如何append到P的local队列中的?m和p的绑定流程又是怎样的?”
新建g流程
以上只是一个go程序的启动流程,当这个程序启动之后,才真正进入到用户程序开始执行业务代码
当在业务代码中,使用了类似于如下的代码:
userFunc := func() {fmt.Println("hi boya!")}go userFunc()
其实在runtime中,是调用了runtime.newproc函数去新建的goroutine
- newproc是newproc1的wraper,主要功能是创建一个新的g
- 在newproc1函数的最后,会去检查是否有空闲的P,如果有空闲的P,则寻找一个M与此P绑定(可能是找一个在sleep的M或者新建一个M)
新建M的流程
- 在newproc1中调用wakep(有空闲的P并且m0已经启动)
- wakep调用startm函数
- startm调用newm去创建新的m
- newm调用newm1,newm1调用newosproc创建m(newosproc根据不同的操作系统有不同的实现)
在newosproc中,我们应该能看到一个熟悉的身影:
这个mstart不就是在主流程中,在汇编中新建的m0,被调用的mstart么?
此时,我们可以继续这么说:
- xxx调用这个新建的mx的mstart(mstart是mstart1的一个wraper, mx是我自己起的名字)
- runtime.mstart1调用schedule进入mx的调度循环
- 进入mx的调度循环后,调度器取出gx,而gx的任务函数为userFunc,运行之
所以,每一个m,都会有一个自己的scheduler(这其实是我想表达的重点)。我以前一直以为所有的m都共享一个scheduler,我是铁憨憨了。
我是不是还差一个新建P的流程?
新建P的流程
其实P在最开始的asm_amd64.s已经通过调用schedinit初始化了所有的p,默认与CPU核心数相同。
所有细节相关的东西,包括m0, g0等解释,在深入golang runtime的调度中都有,强烈推荐大家去读一读
疑问:
自旋的M和休眠的M的区别
