忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么


忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么本文基于 Go 1.13 版本 。
Goroutine 很轻 , 它只需要 2Kb 的内存堆栈即可运行 。 另外 , 它们运行起来也很廉价 , 将一个 Goroutine 切换到另一个的过程不牵涉到很多的操作 。 在深入 Goroutine 切换过程之前 , 让我们回顾一下 Goroutine 的切换在更高的层次上是如何进行的 。
在继续阅读本文之前 , 我强烈建议您阅读我的文章 Go:Goroutine、操作系统线程和 CPU 管理 以了解本文中涉及的一些概念 。
案例Go 根据两种断点将 Goroutine 调度到线程上:

  • 当 Goroutine 因为系统调用、互斥锁或通道而被阻塞时 , goroutine 将进入睡眠模式(等待队列) , 并允许 Go 调度运行另一个处于就绪状态的 goroutine;
  • 在函数调用时 , 如果 Goroutine 必须增加其堆栈 , 这会使 Go 调度另一个 Goroutine 以避免运行中的 Goroutine 独占 CPU 时间片;
在这两种情况下 , 运行调度程序的 g0 会替换当前的 goroutine , 然后选出下一个将要运行的 Goroutine 替换 g0 并在线程上运行 。
有关 g0 的更多信息 , 建议您阅读我的文章 Go:特殊的 Goroutine g0。
将一个运行中的 Goroutine 切换到另一个的过程涉及到两个切换:
  • 将运行中的 g 切换到 g0 :
  • 将 g0 切换到下一个将要运行的 g :
在 Go 中 , goroutine 的切换相当轻便 , 其中需要保存的状态仅仅涉及以下两个:
  • Goroutine 在停止运行前执行的指令 , 程序当前要运行的指令是记录在程序计数器( PC )中的 ,Goroutine 稍后将在同一指令处恢复运行;
  • Goroutine 的堆栈 , 以便在再次运行时还原局部变量;
让我们看看实际情况下的切换是怎样进行的 。
程序计数器这里通过基于通道的 生产者/消费者模式 来举例说明 , 其中一个 Goroutine 产生数据 , 而另一些则消费数据 , 代码如下:
忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么消费者仅仅是打印从 0 到 99 的偶数 。 我们将注意力放在第一个 goroutine(生产者)上 , 它将数字添加到缓冲区 。 当缓冲区已满时 , 它将在发送消息时被阻塞 。 此时 , Go 必须切换到 g0 并调度另一个 Goroutine 来运行 。
如前所述 , Go 首先需要保存当前执行的指令 , 以便稍后在同一条指令上恢复 goroutine 。 程序计数器( PC )保存在 Goroutine 的内部结构中:
忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么可以通过 go tool objdump 命令找到对应的指令及其地址 , 这是生产者的指令:
忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么程序逐条指令的执行直到在函数 runtime.chansend1 处阻塞在通道上 。Go 将当前程序计数器保存到当前 Goroutine 的内部属性中 。 在我们的示例中 , Go 使用运行时的内部地址 0x4268d0 和方法 runtime.chansend1 保存程序计数器:
忘川彼岸|Go:Goroutine 的切换过程实际上涉及了什么然后 , 当 g0 唤醒 Goroutine 时 , 它将在同一指令处继续执行 , 继续将数值循环的推入通道 。 现在 , 让我们将视线移到 Goroutine 切换期间堆栈的管理 。


推荐阅读