5

聊聊 g0

 4 years ago
source link: https://qcrao.com/2020/04/03/talk-about-g0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

很多时候,当我们跟着源码去理解某种事物时,基本上可以认为是以时间顺序展开,这是编年体的逻辑。还有另一种逻辑,纪传体,它以人物为中心编排史事,使得读者更聚焦于某个人物。以一种新的视角,把所有的事情串连起来,令人大呼过瘾。今天我们试着以这样一种逻辑再看 g0。

回顾一下 Go 夜读 第 78 期 ,关于调度器源码分析的内容。我们讲过,与主线程绑定的 M 对应的 g0 的主要作用是提供一个比一般 goroutine 要大的多栈(64K)供 runtime 代码执行。

初始化的过程中,在函数 runtime·rt0_go 里会给主线程的 g0 分配栈空间:

a2qmYjv.png!web

之后,主线程会与 m0 绑定,m0 又与 g0 绑定:

jUzaqmq.png!web

之后,又与 p0 绑定:

zauIru2.png!web

这样,主线程的这一套 GPM 就可以转起来了。接着,就创建了 main goroutine,放入 p0 的本地待运行队列。最后,通过 schedule() 函数进入调度循环。

前面说的是程序初始化的过程中,g0 是如何诞生的。当执行到 main.main() 函数,也说是用户在 main 包下写的 main 函数里,我们随手一句:

go func() {
    // 要做的事
}()

就启动了一个 goroutine 时,在 Go 编译器的作用下,最终会转化成 newproc 函数。在 newproc 函数的内部,会在 g0 栈上调用 newproc1 函数,完成后续的工作。创建完成后,会将新创建的 goroutine 放入 _p_ 的本地待运行队列。

因为新增加了一个 g,这时会尝试去唤醒一个 P 来一起执行任务。判断条件是:

if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
	wakep()
}

即在有空闲 P 以及没有正在“找工作的 M”的情况下,才会尝试去唤醒一个 P。我们又知道,其实 P 的数量在程序运行过程中一般不会变化,所以这里所谓的唤醒其实就是把空闲的 P 利用起来。

通过 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 的 博客 ,题图画得很有阅读的欲望。这篇文章也是参考于 其中的一篇


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK