這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
去年年初的時候曾經寫過一篇關於如何擷取goroutine id的方法: 如何得到goroutine 的 id?, 當時調研了一些一些擷取goid的方法。基本的方法有三種:
- 通過Stack資訊解析出ID
- 通過彙編擷取
runtime·getg
方法的調用結果
- 直接修改運行時的代碼,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 current g
當然這個方法是內部方法,不是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-8MOVQ (TLS), BXMOVQ 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]byteb.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 *gmorebuf gobufdivmod uint32procid uint64gsignal *gsigmask sigsettls [6]uintptrmstartfn func()curg *gcaughtsig uintptrp uintptrnextp uintptrid 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
等資訊。