书接上文:Go的隐秘世界:有Thread为啥还要Goroutine
本文从一个小问题开始:如果一个Go程序只有一个goroutine(没有用 go keyword 启动其他 goroutines),那么 Go runtime 会启动几个线程来执行这个程序呢?
大家可能会说”一个“!因为 Go 的教程说它可以用寥寥几个 threads 执行几万个 goroutines。而且类比 thread 的启动方式 —— 一个进程启动时只有一个 thread,其他 thread 都是从这个 thread 分支出来的。
真的是这样吗?且看下面程序 a.go。
package main
// import "C"
import (
"fmt"
"os"
"time"
)
func main() {
fmt.Println(os.Getpid())
time.Sleep(1000 * time.Second)
}
我在一个terminal里运行 go run a.go。它会打印自己的 pid,然后进入很长时间的等待,使我有时间在另一terminal里用 ps M <pid> 命令查看这个进程有几个线程。在我的有两个 CPU 的 Ubuntu Linux VM(其实是 macOS 上的 Docker container)里,ps M 命令说这个进程启动了 5 个线程!
怎么会有这么多线程?因为 goroutine 的启动模式和 thread 不一样 —— 每个 Go 程序一开始就会启动多个 goroutines。
这里还是没有提及 Cgo。Cgo 对 goroutine 的启动和调度由什么影响呢?先说启动。
如果我们在上面代码里加一行 import "C",也就是说”准备用 Cgo 啦“。重复上述实验。ps M 命令说线程数量变成了 6!增加一行 import "C",就导致线程数量增加了一个!
这个线程是 Go runtime 里的入口函数 main 启动的:https://github.com/golang/go/blob/release-branch.go1.15/src/runtime/proc.go#L170-L189 。我用的是 Go 1.15 做的上述实验,这个源码连接也指向 Go 1.15 的源码。具体的说,是 startTemplateThread 函数启动的。入口函数 main 随后调用用户定义的 main 函数。
// The main goroutine.
func main() {
...
if iscgo {
...
// Start the template thread in case we enter Go from
// a C-created thread and need to create a new thread.
startTemplateThread()
cgocall(_cgo_notify_runtime_init_done, nil)
}
...
}
如果我们进一步看看 startTemplateThread 函数的定义 https://github.com/golang/go/blob/b1be1428dc7d988c2be9006b1cbdf3e513d299b6/src/runtime/proc.go#L1837-L1851 ,会看到线程实际上是它调用 newm 启动的。
func startTemplateThread() {
...
newm(templateThread, nil, -1)
...
}
这个函数名字好奇怪。new 的意思大家都知道,这个 m 是什么呢?
在我们进一步解释 Cgo 不仅影响 goroutines 的启动,而且影响 goroutines 的调度之前,需要把 Go scheduling 机制中的几个术语解释一下。
- M:是 machine 的缩写,指的是 thead。在 Go runtime 里对应 type m struct。源码在这里。
- G:是 goroutine 的缩写。在 Go runtime 里对应 type g struct。源码在这里。
- P: 是 processor 的缩写。在 Go runtime 里对应 type p struct。源码在这里。请注意,P 并不简单对应一个 CPU core,而是指执行 Go code 需要的资源,当然包括 core,也包括在这个 core 上排队等待被执行的 G 们,甚至包括用这个 core 执行这些 G 们的 M(如果 P 的状态不是空闲的话)。
如果一个 Go 程序通过 Cgo 调用了 C 程序,这段 C 程序的执行不需要 P —— 因为 P 是用来执行 Go 程序的资源。不过这段 C 程序和 Go 程序一样,是需要被一个 M 来执行的。换句话说,M 用 P 来执行 Go 程序,M 执行 C 程序的时候不用 P。这一点在后面的章节里我们通过例子来解释。
上面几个概念的列表来自 Go runtime 源码里的注释 https://github.com/golang/go/blob/b1be1428dc7d988c2be9006b1cbdf3e513d299b6/src/runtime/proc.go#L19-L80 。 我对它们的理解有两个来源。一是阅读了两个很不错的系列文章:
- https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
- https://medium.com/a-journey-with-go/tagged/goroutines
二是阅读源码和做试验 —— 类似本文开头的 sample code。
所以,下面的系列文章和这两篇英语的系列文章内容上有交叠,大家不用奇怪。但是交叠不是重叠,结合源码的分析,是这个系列文章的特点。毕竟源码是我们开发 GoTorch 的基础。所以接下来,我们要从科普模式进入 hard core 模式了。
大家准备好看汇编代码了吗?如果准备好了,请点击下文
王益:Go的隐秘世界:Go程序的启动和runtime初始化有疑问加站长微信联系(非本文作者)