go关键词如何初始化一个goroutine
环境
- MacOS:10.13.4
- Golang: version go1.10.1 darwin/amd64
- IDE:VSCode 1.24.0
写在前面
一直想啃一下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的初始化过程。
这部分的源码逻辑比较复杂,无法通过简短的博文讲清楚,建议能自己读一读。
参考
- Golang1.7 Goroutine源码分析 比较详细介绍了goroutine 的proc.go
- go/proc.go runtime.newproc所在的源码文件
- Golang面试解析 讲了一些golang比较细节的问题
- 初探 Go 的编译命令执行过程 简单介绍了go命令执行了哪些事情
- Golang汇编命令解读 用例子简单介绍了Go的汇编