31

Go的隐秘世界:一个Goroutine要几个Thread

 3 years ago
source link: https://studygolang.com/articles/30775
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

书接上文: 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/bl ob/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/bl ob/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/bl ob/b1be1428dc7d988c2be9006b1cbdf3e513d299b6/src/runtime/proc.go#L19-L80 。 我对它们的理解有两个来源。一是阅读了两个很不错的系列文章:

二是阅读源码和做试验 —— 类似本文开头的 sample code。

所以,下面的系列文章和这两篇英语的系列文章内容上有交叠,大家不用奇怪。但是交叠不是重叠,结合源码的分析,是这个系列文章的特点。毕竟源码是我们开发 GoTorch 的基础。所以接下来,我们要从科普模式进入 hard core 模式了。

大家准备好看汇编代码了吗?如果准备好了,请点击下文


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK