golang scheduler 如何以及为什么递归 运行 goroutines in runtime/proc.go:execute?

How and why does golang scheduler recursively run goroutines in runtime/proc.go:execute?

我正在尝试分解 Go 调度程序的工作原理,我在 runtime/proc.go 中看到的是:

  1. schedule函数调用execute到运行一个goroutine
  2. execute 的注释明确表示此函数永远不会 returns。它调用在其中一个汇编文件中定义的 gogo 函数。
  3. gogo 函数执行跳转到新 goroutine 的第一条指令的地址。
  4. 这个goroutine完成后,schedule函数又被调用了,所以我们回到步骤1。

如果我的理解是正确的,那么这个方案是如何避免栈溢出的呢? 它与自动增加大小的 "infinite" 堆栈有关,还是我在这里遗漏了什么?

所以我花了一些时间研究这个主题,现在可以尝试回答我自己的问题了。整个 goroutine 生命周期变得有点复杂:

  1. 新的 goroutines 是在一个名为 g0 的特殊 goroutine 中创建的,它是线程的主 goroutine。对 go func 的任何调用都会将堆栈从调用它的任何当前 goroutine 更改为 g0(这是在 proc.go:newproc 中完成的)。
  2. 创建 goroutine 时(在 proc.go:newproc1 中),它的堆栈(and/or 程序计数器,PC)以看起来的方式构造就像它被 goexit 函数调用一样。这样做是为了保证当 goroutine 完成并且 returns 时,它返回到 goexit.
  3. 当调用schedule并选择一个goroutine到运行时,execute函数执行它(==通过gogo汇编函数跳转到它的地址).
  4. goroutine完成后,returnsgoexit函数,在汇编中实现。
  5. 那个汇编函数调用 proc.go:goexit1(不确定为什么需要这个额外的汇编步骤)。
  6. goexit1 函数将当前堆栈更改为 g0。这是通过调用 mcall ("Machine thread call") 来完成的,它会执行参数中接收到的任何函数。在这种情况下,提供给 mcall 的函数是 goexit0.
  7. 汇编中实现的mcall,跳转到g0的栈帧(SP)地址,执行一次CALLgoexit0
  8. goexit0 函数在 g0 的上下文中执行。它将一个完成的 goroutine 放在一个空闲 goroutine 列表中,如果之前增加了它,则释放它的堆栈。
  9. 然后goexit0再次调用schedule,选择一个goroutine到运行,所以我们回到第3步。

所以这里确实似乎没有递归。调度的 goroutine 本身从不调用 schedule:这是由一个特殊的 goroutine g0 完成的。 我仍然不确定我是否捕捉到了所有细节,因此欢迎评论和补充答案。