go关键词如何初始化一个goroutine

环境

写在前面

一直想啃一下Golang的运行时源码,看了一些以后,脑子里总整理不出一个思路来。大概因为里面包含了太多的概念,调用链路又比较复杂,单靠大脑的几百比特的存储太吃力了。所以打算找一个点慢慢切入。这是系列的第一篇(不知道后面会有几篇=.=),从混编切入,看看goroutine到底是个啥。

main.go及其汇编码

一个main函数

package main

import (
	"fmt"
	"time"
)

func mikk() {
	fmt.Println("kk")
}
func main() {
	fmt.Println("hello")
	// go mikk()
	mikk()
	time.Sleep(1000 * 1000)
}

上面的代码是一个比较简单的main函数,不赘述。关键的地方,在于go mikk()mikk()这两行,如果代码使用mikk()而注释go mikk()(正如当前代码所示),那么就只有一个goroutine;反之,则通过go关键字把mikk()放到了另一个goroutine运行。

汇编main函数

我们可以通过下面的指令把main.go进行汇编,生成相应的汇编文件main.S

go tool compile -S main.go > main.S

对比

当使用mikk()的时候,得到的汇编片段如下:

	0x0058 00088 (main.go:12)	CALL	fmt.Println(SB)
	0x005d 00093 (main.go:14)	PCDATA	$0, $0
	0x005d 00093 (main.go:14)	CALL	"".mikk(SB)
	0x0062 00098 (main.go:15)	MOVQ	$1000000, (SP)

当使用go mikk()的时候,得到的汇编片段如下:

	0x0058 00088 (main.go:12)	CALL	fmt.Println(SB)
	0x005d 00093 (main.go:13)	MOVL	$0, (SP)
	0x0064 00100 (main.go:13)	LEAQ	"".mikk·f(SB), AX
	0x006b 00107 (main.go:13)	MOVQ	AX, 8(SP)
	0x0070 00112 (main.go:13)	PCDATA	$0, $0
	0x0070 00112 (main.go:13)	CALL	runtime.newproc(SB)
	0x0075 00117 (main.go:14)	MOVQ	$1000000, (SP)

简单说明一下"". 代表的是这个函数的命名空间,SB是个伪寄存器,全名为Static Base,代表对应函数的地址

通过对比可以发现,如果没有使用go关键词,会直接调用mikk(SB)函数;如果使用了go关键词,会调用runtime.newproc(SB)函数。

通过查看runtime.newproc(SB)源码func newproc(siz int32, fn *funcval),我们可以知道这个函数需要两个参数,一个是参数个数,一个是方法地址,在汇编代码中分别通过MOVL $0, (SP)MOVQ AX, 8(SP)实现的,0个参数,AX地址所指向的函数

runtime.newproc

通过查看go/proc.go源码中的newproc主要调用了newproc1函数。这里会把调用方(caller)所在的goroutine和pc作为参数传给newproc1

func newproc(siz int32, fn *funcval) {
	argp := add(unsafe.Pointer(&fn), sys.PtrSize)
	gp := getg()
	pc := getcallerpc()
	systemstack(func() {
		newproc1(fn, (*uint8)(argp), siz, gp, pc)
	})
}

创建goroutine的工作大部分在newproc1中完成。它会首先从freeG列表中尝试获取一个free的goroutine(重复利用资源,可以减少malloc的次数,降低时间消耗),只有获取不到的时候才会重新在堆栈中搞一块新的内存并初始化gouroutine。

把goroutine的栈初始化,并把各项属性设置适当的值以后,就可以把这个goroutine加入到当前P的G队列了。

// 创建一个新的goroutine运行fn,参数开始于argp,共有narg个字节
// 最后把创建的g放到g队列等待运行
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	// 获得当前的G
	_g_ := getg()
...
	// 从P的freeG队列中拿一个G
	_p_ := _g_.m.p.ptr()
	newg := gfget(_p_)
...
	// 将G加入P的runnable G队列
	runqput(_p_, newg, true)
...
}

小结

本文从汇编代码入手,发现了runtime.newproc这一条线索,通过查看相应的源码,简单介绍了goroutine的初始化过程。

这部分的源码逻辑比较复杂,无法通过简短的博文讲清楚,建议能自己读一读。

参考