再谈谈获取 goroutine id 的方法

鸟窝 2017-09-05 08:58

去年年初的时候曾经写过一篇关于如何获取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来大致地跟踪程序并发执行的时候的状况。

12345678910111213141516171819202122
package mainimport (	"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 currentg

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

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

https://github.com/petermattis/goid/blob/master/goid_go1.5plus.s
12345678910
// +build amd64 amd64p32// +build go1.5#include "textflag.h"// func getg() uintptrTEXT ·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,所以只需实现:

1234
func Get() int64 {	gg := (*g)(unsafe.Pointer(getg()))	return gg.goid}

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

123456789101112131415161718192021222324
package pkgimport (	"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)])	}}

性能比较结果:

12
BenchmarkASM-4    	300000000	         3.70 ns/op	       0 B/op	       0 allocs/opBenchmarkSlow-4   	  300000	      4071 ns/op	       1 B/op	       1 allocs/op

一千多倍的差距。

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

1234567891011121314151617181920212223
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,比上面的方式更简单,性能还好:

123456789
#include "go_asm.h"#include "textflag.h"// func Get() int64TEXT ·Get(SB),NOSPLIT,$0-8	MOVQ (TLS), R14	MOVQ g_goid(R14), R13	MOVQ R13, ret+0(FP)	RET

[返回] [原文链接]