不少时候,当咱们跟着源码去理解某种事物时,基本上能够认为是以时间顺序展开,这是编年体的逻辑。还有另外一种逻辑,纪传体,它以人物为中心编排史事,使得读者更聚焦于某我的物。以一种新的视角,把全部的事情串连起来,使人大呼过瘾。今天咱们试着以这样一种逻辑再看 g0。linux
回顾一下 Go 夜读第 78 期,关于调度器源码分析的内容。咱们讲过,与主线程绑定的 M 对应的 g0 的主要做用是提供一个比通常 goroutine 要大的多栈(64K)供 runtime 代码执行。git
初始化的过程当中,在函数 runtime·rt0_go
里会给主线程的 g0 分配栈空间:github
以后,主线程会与 m0 绑定,m0 又与 g0 绑定:golang
以后,又与 p0 绑定:markdown
这样,主线程的这一套 GPM 就能够转起来了。接着,就建立了 main goroutine,放入 p0 的本地待运行队列。最后,经过 schedule()
函数进入调度循环。函数
前面说的是程序初始化的过程当中,g0 是如何诞生的。当执行到 main.main()
函数,也说是用户在 main 包下写的 main 函数里,咱们随手一句:oop
go func() {
// 要作的事
}()
复制代码
就启动了一个 goroutine 时,在 Go 编译器的做用下,最终会转化成 newproc 函数。在 newproc 函数的内部,会在 g0 栈上调用 newproc1
函数,完成后续的工做。建立完成后,会将新建立的 goroutine 放入 _p_
的本地待运行队列。源码分析
由于新增长了一个 g,这时会尝试去唤醒一个 P 来一块儿执行任务。判断条件是:atom
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
复制代码
即在有空闲 P 以及没有正在“找工做的 M”的状况下,才会尝试去唤醒一个 P。咱们又知道,其实 P 的数量在程序运行过程当中通常不会变化,因此这里所谓的唤醒其实就是把空闲的 P 利用起来。spa
经过 wakep() -> startm() -> newm() -> allocm() -> malg()
这条链路建立 g0,这里 g0 的栈大小实际上为 8KB
。
mp.g0 = malg(8192 * sys.StackGuardMultiplier) // sys.StackGuardMultiplier 在 linux 里为 1
复制代码
g0
做为一个特殊的 goroutine,为 scheduler 执行调度循环提供了场地(栈)。对于一个线程来讲,g0 老是它第一个建立的 goroutine。以后,它会不断地寻找其余普通的 goroutine 来执行,直到进程退出。
当须要执行一些任务,且不想扩栈时,就能够用到 g0 了,由于 g0 的栈比较大。g0 其余的一些“职责”有:建立 goroutine、deferproc 函数里新建 _defer、垃圾回收相关的工做(例如 stw、扫描 goroutine 的执行栈、一些标识清扫的工做、栈增加)等等。
由于 g0 这样一个特殊的 goroutine 所作的工做,使得 Go 程序运行地更快。
注:最近在 medium 上看到了一个很是赞的关于 Go 的博客,题图画得颇有阅读的欲望。这篇文章也是参考于其中的一篇。