g0 特殊的goroutine
admin
- 2 minutes read - 348 words在上篇 《golang中G、P、M 和 sched 三者的数据结构》文章中,我们介绍了G、M 和 P 的数据结构,其中M结构体中第一个字段是 g0,这个字段也是一个 goroutine,但和普通的 goroutine 有一些区别,它主要用来实现对 goroutine 进行调度,下面我们将介绍它是如何实现调度goroutine的。
另外还有一个 m0 , 它是一个全局变量,与 g0 的区别如下
M0 与 g0的区别
本文主要翻译自 Go: g0, Special Goroutine 一文,有兴趣的可以查阅原文,作者有一系列高质量的文章推荐大家都阅读一遍。ℹ️ 本文基于 Go 1.13。
我们知道在Golang中所有的goroutine的运行都是由调度器来负责管理的,go调度器尝试为所有的goroutine来分配运行时间,当有goroutine被阻塞或终止时,调度器会通过对goroutine 进行调度以此来保证所有CPU都处于忙碌状态,避免有CPU空闲状态浪费时间。
goroutine 切换规则
在此之前我们需要记住一些goroutine切换规则。runtime源码
// src/runtime/stubs.go
// mcall switches from the g to the g0 stack and invokes fn(g),
// where g is the goroutine that made the call.
// mcall saves g's current PC/SP in g->sched so that it can be restored later.
// It is up to fn to arrange for that later execution, typically by recording
// g in a data structure, causing something to call ready(g) later.
// mcall returns to the original goroutine g later, when g has been rescheduled.
// fn must not return at all; typically it ends by calling schedule, to let the m
// run other goroutines.
//
// mcall can only be called from g stacks (not g0, not gsignal).
//
// This must NOT be go:noescape: if fn is a stack-allocated closure,
// fn puts g on a run queue, and g executes before fn returns, the
// closure will be invalidated while it is still executing.
func mcall(fn func(*g))
对 mcall() 函数注释的翻译请参考文章 Runtime: 当一个goroutine 运行结束后会发生什么
一、将一个运行中的 Goroutine 切换到另一个的过程涉及到两个切换:
- 将运行中的
g切换到g0
- 将
g0切换到下一个将要运行的g
二、在 Go 中,goroutine 的切换成本很低,每次切换之前都需要对当前G的状态(PC/SP)进行存储(g.sched),以便下次恢复运行时读取当前G的上下文信息:
- Goroutine 在停止运行当前执行的指令时,需先将当前运行的指令存储在程序计数器(
PC),同时与堆栈指针(SP)存储到g.sched字段。等 Goroutine 恢复执行时,需将程序计数器(PC) 和堆栈指针(SP)从g.sched字段中再次读取出来,并将程序跳转到PC地址继续执行; - Goroutine 的堆栈,以便在再次运行时还原局部变量;
如果对这一块还不清楚的话, 推荐阅读 Go:Goroutine 的切换过程实际上涉及了什么
让我们看看实际情况下goroutine的切换是怎样进行的。
调度 goroutine
Go 使用 GOMAXPROCES 变量控制运行的系统线程个数,这就意味着Go必须在每个运行着的系统线程上调度和管理goroutine。这个调度工作被委托给一个特殊的goroutine,也就是 g0。调度过程中 g0 会把就绪状态的goroutine调度到系统线程上运行。因此g0是每个os线程创建的第一个 goroutine。
g0
为了更好理解 g0 的调度策略,假如现在有一个goroutine #G7,正在运行用户代码
ch := make(chan int)
[...]
ch <- v
当前 goroutine #G7 往 channel 里写入一个值,但此时接收端尚未准备好,此时goroutine将进行parked,即处于等待状态( waiting mode ),G7状态变由 _Grunning 为处于_Gwaiting 阻塞状态。
g0
然后g0介入,g0 会替换 goroutine 并进行第一轮调度(第一次切换:将 G7 切换到 g0)。
g0
g0 会获取一个新的goroutine。优先从当前与 m 绑定的 p 中的本地运行队列 q.runq 中获取一个新的 goroutine #G2(第二次切换:g0 再切换到 G2)
g0
一旦有接收器读取到channel 中的数据,goroutine #G7 将会立即解除阻塞状态。
v := <-ch
收到消息的goroutine #G7 将切换到 g0 ,并通过将其放置在本地队列中, 并将状态由 _Gwaiting 变为 _Grunnable 。(言外之意想让哪个goroutine运行 g0 说了算)
g0
至此,对 goroutine #G7 的一轮调度完成,G2 继续执行后面的用户代码。如果在 G2 里再也遇到类似阻塞的情况下,则重新发起一轮调度。
g0 职责
g0与普通的goroutine不同,g0有着固定且比较大的栈,这样go就可以在需要更大栈的时候就可以直接使用,不需要进行栈增长。
g0职责有:
Goroutine创建。 当调用
go func(){ ... }()或go myFunction()时,Go会将函数的创建委托给g0,随后再将其放置在本地队列中。新创建的goroutine优先执行,并放置在本地队列的顶部。为什么放在队顶可参考 Go:并发与调度器亲和性( Go: Concurrency & Scheduler Affinity )
g0Defer方法分配。
GC操作,例如stw,扫描goroutine的栈以及一些标清操作。
栈增长。在需要时,Go会增加goroutine的大小。该操作由
g0在prolog方法中完成。
这个特殊的goroutine g0涉及许多其他操作(大量分配,cgo等),使我们的程序可以更高效地管理操作,并且需要更大的栈,以保持我们的程序在低内存下更加高效。
参考文章
- https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8
- Go:Goroutine 的切换过程实际上涉及了什么
- Go:并发与调度器亲和性( Go: Concurrency & Scheduler Affinity )
- Go netpoller 网络模型之源码全面解析
- Go的启动周期M0和G0