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

王益 · · 1108 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

书接上文: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 启动的:github.com/golang/go/bl 。我用的是 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 函数的定义 github.com/golang/go/bl ,会看到线程实际上是它调用 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 源码里的注释 github.com/golang/go/bl 。 我对它们的理解有两个来源。一是阅读了两个很不错的系列文章:

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

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

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

王益:Go的隐秘世界:Go程序的启动和runtime初始化zhuanlan.zhihu.com图标


有疑问加站长微信联系(非本文作者)

本文来自:知乎专栏

感谢作者:王益

查看原文:Go的隐秘世界:一个Goroutine要几个Thread

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1108 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传