go coroutine - yaokun123/php-wiki GitHub Wiki

协程的实现原理

Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱。虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的。这篇文章将通过分析golang的源代码来讲解协程的实现原理

这个系列分析的golang源代码是Google官方的实现的1.9.2版本, 不适用于其他版本和gccgo等其他实现

一、核心概念

要理解协程的实现, 首先需要了解go中的三个非常重要的概念, 它们分别是G, M和P,没有看过golang源代码的可能会对它们感到陌生, 这三项是协程最主要的组成部分, 它们在golang的源代码中无处不在。

1、G (goroutine)

G是goroutine的头文字, goroutine可以解释为受管理的轻量线程, goroutine使用go关键词创建。

举例来说, func main() { go other() }, 这段代码创建了两个goroutine,一个是main, 另一个是other, 注意main本身也是一个goroutine。

goroutine的新建, 休眠, 恢复, 停止都受到go运行时的管理。

goroutine执行异步操作时会进入休眠状态, 待操作完成后再恢复, 无需占用系统线程。

goroutine新建或恢复时会添加到运行队列, 等待M取出并运行

2、M (machine)

M是machine的头文字, 在当前版本的golang中等同于系统线程。

M可以运行两种代码:

  • go代码, 即goroutine, M运行go代码需要一个P
  • 原生代码, 例如阻塞的syscall, M运行原生代码不需要P

M会从运行队列中取出G, 然后运行G, 如果G运行完毕或者进入休眠状态, 则从运行队列中取出下一个G运行, 周而复始。

有时候G需要调用一些无法避免阻塞的原生代码, 这时M会释放持有的P并进入阻塞状态, 其他M会取得这个P并继续运行队列中的G。

go需要保证有足够的M可以运行G, 不让CPU闲着, 也需要保证G的数量不能过多。

3、P (process)

P是process的头文字, 代表M运行G所需要的资源。一些讲解协程的文章把P理解为cpu核心, 其实这是错误的。

虽然P的数量默认等于cpu核心数, 但可以通过环境变量GOMAXPROC修改, 在实际运行时P跟cpu核心并无任何关联.

P也可以理解为控制go代码的并行度的机制:

  • 如果P的数量等于1, 代表当前最多只能有一个线程(M)执行go代码,
  • 如果P的数量等于2, 代表当前最多只能有两个线程(M)执行go代码

执行原生代码的线程数量不受P控制

因为同一时间只有一个线程(M)可以拥有P, P中的数据都是锁自由(lock free)的, 读写这些数据的效率会非常的高。

二、数据结构

在讲解协程的工作流程之前, 还需要理解一些内部的数据结构

1、G的状态

  • 空闲中(_Gidle): 表示G刚刚新建, 仍未初始化
  • 待运行(_Grunnable): 表示G在运行队列中, 等待M取出并运行
  • 运行中(_Grunning): 表示M正在运行这个G, 这时候M会拥有一个P
  • 系统调用中(_Gsyscall): 表示M正在运行这个G发起的系统调用, 这时候M并不拥有P
  • 等待中(_Gwaiting): 表示G在等待某些条件完成, 这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
  • 已中止(_Gdead): 表示G未被使用, 可能已执行完毕(并在freelist中等待下次复用)
  • 栈复制中(_Gcopystack): 表示G正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)

2、M的状态

M并没有像G和P一样的状态标记, 但可以认为一个M有以下的状态:

  • 自旋中(spinning): M正在从运行队列获取G, 这时候M会拥有一个P
  • 执行go代码中: M正在执行go代码, 这时候M会拥有一个P
  • 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P
  • 休眠中: M发现无待运行的G时会进入休眠, 并添加到空闲M链表中, 这时M并不拥有P

自旋中(spinning)这个状态非常重要, 是否需要唤醒或者创建新的M取决于当前自旋中的M的数量

3、P的状态

  • 空闲中(_Pidle): 当M发现无待运行的G时会进入休眠, 这时M拥有的P会变为空闲并加到空闲P链表中
  • 运行中(_Prunning): 当M拥有了一个P后, 这个P的状态就会变为运行中, M运行G会使用这个P中的资源
  • 系统调用中(_Psyscall): 当go调用原生代码, 原生代码又反过来调用go代码时, 使用的P会变为此状态
  • GC停止中(_Pgcstop): 当gc停止了整个世界(STW)时, P会变为此状态
  • 已中止(_Pdead): 当P的数量在运行时改变, 且数量减少时多余的P会变为此状态