Go语言并发模型 G(Goroutine)

G 即 goroutine,又称为轻量级的用户线程,或者协程。它是一个可以并发执行的实体,底层使用 coroutine 实现并发,coroutine 是一种运行在用户态的用户线程。

Go 底层选择使用 coroutine,主要因为它具有以下特点:

  • 运行在用户态,避免了内核态和用户态的切换导致的成本。
  • 可以由语言和框架层进行调度。
  • 更小的栈空间允许创建大量的实例。

用户空间线程的调度不是由操作系统来完成的,而是由用户空间的调度器执行。 Go 调度器,又称为运行时系统(runtime)。一个 Go 程序中只会存在一个调度器实例。它拥有自己的结构,同时依次提供了很多重要的运行时功能。它对网络 IO 库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。开发者只需要使用 go 语句向 Go 的运行时系统提交了一个并发任务,Go 运行时系统局会按照要求并发地执行它。

Go 的编译器会把 go 语句变成对内部函数 newproc 的调用,并把 go 函数及其参数都作为参数传递给这个函数。

Go 运行时系统在接到这样一个调用之后,会先检查 go 函数及其参数的合法性,然后试图从本地 P 的自由 G 列表和调度器的自由 G 列表获取可用的 G,如果没有获取到,就新建一个G。

Go 运行时系统持有一个 G 的全局列表(runtime.allgs)。新建的 G 会在第一时间被加入该列表。这个全局列表的主要作用是:集中存放当前运行时系统中的所有 G 的指针。无论用于封装当前这个 go 函数的 G 是否是新创建的,运行时系统都会对它进行一次初始化,包括关联 go 函数以及设置该 G 的状态和 ID 等步骤。在初始化完成后,这个 G 会立即被存储到本地 P 的 runnext 字段中。runnext 字段用于存放新的G,以求更早地运行它。如果这时 runnext 字段已存有一个 G,那么这个已有的 G 就会被移动到 P 的可运行 G 队列的末尾。如果该队列已满,那么这个 G 就只能追加到调度器的可运行 G 队列中了。

每一个G都会由运行时系统根据其实际状况设置不同的状态,其主要状态如下。

  • Gidle 表示当前 G 刚被新分配,但还未初始化。
  • Grunnable 表示当前 G 正在可运行队列中等待运行。
  • Grunning 表示当前 G 正在运行。
  • Gsyscall 表示当前 G 正在执行某个系统调用。
  • Gwaiting 表示当前 G 正在阻塞。
  • Gdead 表示当前 G 正在闲置。
  • Gcopystack 表示当前 G 的栈正被移动,移动的原因可能是栈的扩展或收缩。

除了上述状态,还有一个称为 Gscan 的状态。不过这个状态并不能独立存在,而是组合状态的一部分。比如,GscanGrunnable 组合成 Gscanrunnable 状态,代表当前 G 正等待运行,同时它的栈正被扫描,扫描的原因一般是GC(垃圾回收)任务的执行。又比如,GscanGrunning 组合成 Gscanrunning 状态,表示正处于 Grunning 状态的当前 G 的栈要被 GC 扫描时的一个短暂时刻。

在运行时系统想用一个 G 封装 go 函数的时候,会先对这个 G 进行初始化。一旦该 G 准备就绪,其状态就会被设置成 Grunnable。也就是说,一个 G 真正开始被使用是在其状态设置为 Grunnable 之后。下图展示了 G 在其生命周期内的状态转换情况。

Go语言并发模型 G(Goroutine)

一个 G 在运行的过程中,是否会等待某个事件以及会等待什么样的事件,完全由其封装的go函数决定。例如,如果这个函数中包含对通道值的操作,那么在执行到对应代码的时候,这个 G 就有可能进入Gwaiting状态。这可能是在等待从通道类型值中接收值,也可能是在等待向通道类型值发送值。又例如,涉及网络 I/O 的时候也会导致相应的 G 进入Gwaiting状态。此外,操纵定时器(time.Timer)和调用time.Sleep函数同样会造成相应 G 的等待。在事件到来之后,G 会被“唤醒”并被转换至Grunnable状态。待时机到来时,它会被再次运行。

G 在退出系统调用时的状态转换要比上述情况复杂一些。运行时系统会先尝试直接运行这个 G,仅当无法直接运行的时候,才会把它转换为Grunnable状态并放入调度器的自由 G 列表中。显然,对这样一个 G 来说,在其退出系统调用之时就立即被恢复运行再好不过了。运行时系统当然会为此做出一些努力,不过即使努力失败了,该 G 也还是会在实时的调度过程中被发现并运行。

最后,值得一提的是,进入死亡状态(Gdead)的 G 是可以重新初始化并使用的。相比之下,P 在进入死亡状态(Pdead)之后,就只能面临销毁的结局。由此也可以说明Gdead状态与Pdead状态所表达的含义截然不同。处于Gdead状态的 G 会被放入本地 P 或调度器的自由 G 列表,这是它们被重用的前提条件。

Go 的线程实现模型,有三个核心的元素 M、P、G,它们共同支撑起了这个线程模型的框架。其中,G 是 goroutine 的缩写,通常称为 “协程”。关于协程、线程和进程三者的异同,可以参照 “进程、线程和协程 ...