再谈谈获取 goroutine id 的方法

去年年初的时候曾经写过一篇关于如何获取goroutine id的方法: 如何得到goroutine 的 id?, 当时调研了一些一些获取goid的方法。基本的方法有三种:

  1. 通过Stack信息解析出ID
  2. 通过汇编获取runtime·getg方法的调用结果
  3. 直接修改运行时的代码,export一个可以外部调用的GoID()方法

每个方式都有些问题, #1比较慢, #2因为是hack的方式(Go team并不想暴露go id的信息), 针对不同的Go版本中需要特殊的hack手段, #3需要定制Go运行时,不通用。当时的petermattis/goid提供了 #2 的方法, 但是只能在 go 1.3中才起作用,所以只能选择#1的方式获取go id。

最近一年来, petermattis更新了他的代码,逐步增加了对 Go 1.4、1.5、1.6、1.7、1.8、1.9的支持,同时也提供了#1的方法,在#2方法不起作用的时候作为备选,所以我们可以在当前的所有的版本中可以使用stable的获取go id的方法了。

你或许会遇到一些需要使用Go ID的场景, 比如在多goroutine长时间运行任务的时候,我们通过日志来跟踪任务的执行情况,可以通过go id来大致地跟踪程序并发执行的时候的状况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"log"
"time"
"github.com/petermattis/goid"
)
func main() {
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 1000000; j++ {
log.Printf("[#%d] %d", goid.Get(), j)
time.Sleep(10e9)
}
}()
}
select {}
}

依照Go代码中的文档HACKING, go运行时中实现了一个getg()方法,可以获取当前的goroutine:

getg() alone returns the current g

当然这个方法是内部方法,不是exported,不能被外部的调用,而且返回的数据结构也是未exported的。如果有办法暴露出这个方法,问题就解决了。

petermattis/goid 模仿runtime.getg暴露出一个getg的方法

https://github.com/petermattis/goid/blob/master/goid_go1.5plus.s
1
2
3
4
5
6
7
8
9
10
// +build amd64 amd64p32
// +build go1.5
#include "textflag.h"
// func getg() uintptr
TEXT ·getg(SB),NOSPLIT,$0-8
MOVQ (TLS), BX
MOVQ BX, ret+0(FP)
RET

上面的代码实际是将当前的goroutine的结构体的指针(TLS)返回。

参考: Golang Internals 以及中文翻译 Go语言内幕
TLS 其实是线程本地存储 (Thread Local Storage )的缩写。这个技术在很多编程语言中都有用到(请参考这里)。简单地说,它为每个线程提供了一个这样的变量,不同变量用于指向不同的内存区域。

在 Go 语言中,TLS 存储了一个 G 结构体的指针。这个指针所指向的结构体包括 Go 例程的内部细节(后面会详细谈到这些内容)。因此,当在不同的例程中访问该变量时,实际访问的是该例程相应的变量所指向的结构体。链接器知道这个变量所在的位置,前面的指令中移动到 CX 寄存器的就是这个变量。对于 AMD64,TLS 是用 FS 寄存器来实现的, 所在我们前面看到的命令实际上可以翻译为 MOVQ FS, CX。

不同的Go版本获取的数据结构可能是不同的,所以petermattis/goid针对1.5、1.6、1.9有变动的版本定制了不同的数据结构,因为我们只需要得到goroutine的ID,所以只需实现:

1
2
3
4
func Get() int64 {
gg := (*g)(unsafe.Pointer(getg()))
return gg.goid
}

我比较了一下#1和#2这两种实现方式的性能,差距还是非常大的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package pkg
import (
"runtime"
"testing"
"github.com/petermattis/goid"
)
func BenchmarkASM(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
goid.Get()
}
}
func BenchmarkSlow(b *testing.B) {
b.ReportAllocs()
var buf [64]byte
b.ResetTimer()
for i := 0; i < b.N; i++ {
goid.ExtractGID(buf[:runtime.Stack(buf[:], false)])
}
}

性能比较结果:

1
2
BenchmarkASM-4 300000000 3.70 ns/op 0 B/op 0 allocs/op
BenchmarkSlow-4 300000 4071 ns/op 1 B/op 1 allocs/op

一千多倍的差距。

petermattis/goid这种hack的方式可以暴露更多的运行时的细节,比如我们可以扩展一下,得到当前哪个m正在运行,甚至可以得到当前的线程的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type m struct {
g0 *g
morebuf gobuf
divmod uint32
procid uint64
gsignal *g
sigmask sigset
tls [6]uintptr
mstartfn func()
curg *g
caughtsig uintptr
p uintptr
nextp uintptr
id int32
}
func GetM() int32 {
gg := (*g)(unsafe.Pointer(getg()))
m := (*m)(unsafe.Pointer(gg.m))
return m.id
}

sigset在不同的平台的大小是不一样的,可以参考os_*.go中各平台的定义。上面是得到m的ID, 更全的m的结构定义海包括thread等信息。

2017-09-05 更新

petermattis 最近做了更新, 利用go本身的g_goid宏,直接利用汇编就可以获取goroutine id,比上面的方式更简单,性能还好:

1
2
3
4
5
6
7
8
9
#include "go_asm.h"
#include "textflag.h"
// func Get() int64
TEXT ·Get(SB),NOSPLIT,$0-8
MOVQ (TLS), R14
MOVQ g_goid(R14), R13
MOVQ R13, ret+0(FP)
RET