原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-08-goroutine-id.html
在操作系統(tǒng)中,每個(gè)進(jìn)程都會(huì)有一個(gè)唯一的進(jìn)程編號(hào),每個(gè)線(xiàn)程也有自己唯一的線(xiàn)程編號(hào)。同樣在 Go 語(yǔ)言中,每個(gè) Goroutine 也有自己唯一的 Go 程編號(hào),這個(gè)編號(hào)在 panic 等場(chǎng)景下經(jīng)常遇到。雖然 Goroutine 有內(nèi)在的編號(hào),但是 Go 語(yǔ)言卻刻意沒(méi)有提供獲取該編號(hào)的接口。本節(jié)我們嘗試通過(guò) Go 匯編語(yǔ)言獲取 Goroutine ID。
根據(jù)官方的相關(guān)資料顯示,Go 語(yǔ)言刻意沒(méi)有提供 goid 的原因是為了避免被濫用。因?yàn)榇蟛糠钟脩?hù)在輕松拿到 goid 之后,在之后的編程中會(huì)不自覺(jué)地編寫(xiě)出強(qiáng)依賴(lài) goid 的代碼。強(qiáng)依賴(lài) goid 將導(dǎo)致這些代碼不好移植,同時(shí)也會(huì)導(dǎo)致并發(fā)模型復(fù)雜化。同時(shí),Go 語(yǔ)言中可能同時(shí)存在海量的 Goroutine,但是每個(gè) Goroutine 何時(shí)被銷(xiāo)毀并不好實(shí)時(shí)監(jiān)控,這也會(huì)導(dǎo)致依賴(lài) goid 的資源無(wú)法很好地自動(dòng)回收(需要手工回收)。不過(guò)如果你是 Go 匯編語(yǔ)言用戶(hù),則完全可以忽略這些借口。
為了便于理解,我們先嘗試用純 Go 的方式獲取 goid。使用純 Go 的方式獲取 goid 的方式雖然性能較低,但是代碼有著很好的移植性,同時(shí)也可以用于測(cè)試驗(yàn)證其它方式獲取的 goid 是否正確。
每個(gè) Go 語(yǔ)言用戶(hù)應(yīng)該都知道 panic 函數(shù)。調(diào)用 panic 函數(shù)將導(dǎo)致 Goroutine 異常,如果 panic 在傳遞到 Goroutine 的根函數(shù)還沒(méi)有被 recover 函數(shù)處理掉,那么運(yùn)行時(shí)將打印相關(guān)的異常和棧信息并退出 Goroutine。
下面我們構(gòu)造一個(gè)簡(jiǎn)單的例子,通過(guò) panic 來(lái)輸出 goid:
package main
func main() {
panic("goid")
}
運(yùn)行后將輸出以下信息:
panic: goid
goroutine 1 [running]:
main.main()
/path/to/main.go:4 +0x40
我們可以猜測(cè) Panic 輸出信息 goroutine 1 [running]
中的 1 就是 goid。但是如何才能在程序中獲取 panic 的輸出信息呢?其實(shí)上述信息只是當(dāng)前函數(shù)調(diào)用棧幀的文字化描述,runtime.Stack 函數(shù)提供了獲取該信息的功能。
我們基于 runtime.Stack 函數(shù)重新構(gòu)造一個(gè)例子,通過(guò)輸出當(dāng)前棧幀的信息來(lái)輸出 goid:
package main
import "runtime"
func main() {
var buf = make([]byte, 64)
var stk = buf[:runtime.Stack(buf, false)]
print(string(stk))
}
運(yùn)行后將輸出以下信息:
goroutine 1 [running]:
main.main()
/path/to/main.g
因此從 runtime.Stack 獲取的字符串中就可以很容易解析出 goid 信息:
func GetGoid() int64 {
var (
buf [64]byte
n = runtime.Stack(buf[:], false)
stk = strings.TrimPrefix(string(buf[:n]), "goroutine")
)
idField := strings.Fields(stk)[0]
id, err := strconv.Atoi(idField)
if err != nil {
panic(fmt.Errorf("can not get goroutine id: %v", err))
}
return int64(id)
}
GetGoid 函數(shù)的細(xì)節(jié)我們不再贅述。需要補(bǔ)充說(shuō)明的是 runtime.Stack
函數(shù)不僅僅可以獲取當(dāng)前 Goroutine 的棧信息,還可以獲取全部 Goroutine 的棧信息(通過(guò)第二個(gè)參數(shù)控制)。同時(shí)在 Go 語(yǔ)言?xún)?nèi)部的 net/http2.curGoroutineID 函數(shù)正是采用類(lèi)似方式獲取的
goid。
根據(jù)官方的 Go 匯編語(yǔ)言文檔,每個(gè)運(yùn)行的 Goroutine 結(jié)構(gòu)的 g 指針保存在當(dāng)前運(yùn)行 Goroutine 的系統(tǒng)線(xiàn)程的局部存儲(chǔ) TLS 中??梢韵全@取 TLS 線(xiàn)程局部存儲(chǔ),然后再?gòu)?TLS 中獲取 g 結(jié)構(gòu)的指針,最后從 g 結(jié)構(gòu)中取出 goid。
下面是參考 runtime 包中定義的 get_tls 宏獲取 g 指針:
get_tls(CX)
MOVQ g(CX), AX // Move g into AX.
其中 get_tls 是一個(gè)宏函數(shù),在 runtime/go_tls.h 頭文件中定義。
對(duì)于 AMD64 平臺(tái),get_tls 宏函數(shù)定義如下:
#ifdef GOARCH_amd64
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#endif
將 get_tls 宏函數(shù)展開(kāi)之后,獲取 g 指針的代碼如下:
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
其實(shí) TLS 類(lèi)似線(xiàn)程局部存儲(chǔ)的地址,地址對(duì)應(yīng)的內(nèi)存里的數(shù)據(jù)才是 g 指針。我們還可以更直接一點(diǎn):
MOVQ (TLS), AX
基于上述方法可以包裝一個(gè) getg 函數(shù),用于獲取 g 指針:
// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, $0-8
MOVQ (TLS), AX
MOVQ AX, ret+0(FP)
RET
然后在 Go 代碼中通過(guò) goid 成員在 g 結(jié)構(gòu)體中的偏移量來(lái)獲取 goid 的值:
const g_goid_offset = 152 // Go1.10
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
其中 g_goid_offset
是 goid 成員的偏移量,g 結(jié)構(gòu)參考 runtime/runtime2.go。
在 Go1.10 版本,goid 的偏移量是 152 字節(jié)。因此上述代碼只能正確運(yùn)行在 goid 偏移量也是 152 字節(jié)的 Go 版本中。根據(jù)湯普森大神的神諭,枚舉和暴力窮舉是解決一切疑難雜癥的萬(wàn)金油。我們也可以將 goid 的偏移保存到表格中,然后根據(jù) Go 版本號(hào)查詢(xún) goid 的偏移量。
下面是改進(jìn)后的代碼:
var offsetDictMap = map[string]int64{
"go1.10": 152,
"go1.9": 152,
"go1.8": 192,
}
var g_goid_offset = func() int64 {
goversion := runtime.Version()
for key, off := range offsetDictMap {
if goversion == key || strings.HasPrefix(goversion, key) {
return off
}
}
panic("unsupported go version:"+goversion)
}()
現(xiàn)在的 goid 偏移量已經(jīng)終于可以自動(dòng)適配已經(jīng)發(fā)布的 Go 語(yǔ)言版本。
枚舉和暴力窮舉雖然夠直接,但是對(duì)于正在開(kāi)發(fā)中的未發(fā)布的 Go 版本支持并不好,我們無(wú)法提前知曉開(kāi)發(fā)中的某個(gè)版本的 goid 成員的偏移量。
如果是在 runtime 包內(nèi)部,我們可以通過(guò) unsafe.OffsetOf(g.goid)
直接獲取成員的偏移量。也可以通過(guò)反射獲取 g 結(jié)構(gòu)體的類(lèi)型,然后通過(guò)類(lèi)型查詢(xún)某個(gè)成員的偏移量。因?yàn)?g 結(jié)構(gòu)體是一個(gè)內(nèi)部類(lèi)型,Go 代碼無(wú)法從外部包獲取 g 結(jié)構(gòu)體的類(lèi)型信息。但是在 Go 匯編語(yǔ)言中,我們是可以看到全部的符號(hào)的,因此理論上我們也可以獲取 g 結(jié)構(gòu)體的類(lèi)型信息。
在任意的類(lèi)型被定義之后,Go 語(yǔ)言都會(huì)為該類(lèi)型生成對(duì)應(yīng)的類(lèi)型信息。比如 g 結(jié)構(gòu)體會(huì)生成一個(gè) type·runtime·g
標(biāo)識(shí)符表示 g 結(jié)構(gòu)體的值類(lèi)型信息,同時(shí)還有一個(gè) type·*runtime·g
標(biāo)識(shí)符表示指針類(lèi)型的信息。如果 g 結(jié)構(gòu)體帶有方法,那么同時(shí)還會(huì)生成 go.itab.runtime.g
和 go.itab.*runtime.g
類(lèi)型信息,用于表示帶方法的類(lèi)型信息。
如果我們能夠拿到表示 g 結(jié)構(gòu)體類(lèi)型的 type·runtime·g
和 g 指針,那么就可以構(gòu)造 g 對(duì)象的接口。下面是改進(jìn)的 getg 函數(shù),返回 g 指針對(duì)象的接口:
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret+0(FP)
MOVQ BX, ret+8(FP)
RET
其中 AX 寄存器對(duì)應(yīng) g 指針,BX 寄存器對(duì)應(yīng) g 結(jié)構(gòu)體的類(lèi)型。然后通過(guò) runtime·convT2E 函數(shù)將類(lèi)型轉(zhuǎn)為接口。因?yàn)槲覀兪褂玫牟皇?g 結(jié)構(gòu)體指針類(lèi)型,因此返回的接口表示的 g 結(jié)構(gòu)體值類(lèi)型。理論上我們也可以構(gòu)造 g 指針類(lèi)型的接口,但是因?yàn)?Go 匯編語(yǔ)言的限制,我們無(wú)法使用 type·*runtime·g
標(biāo)識(shí)符。
基于 g 返回的接口,就可以容易獲取 goid 了:
func GetGoid() int64 {
g := getg()
gid := reflect.ValueOf(g).FieldByName("goid").Int()
return goid
}
上述代碼通過(guò)反射直接獲取 goid,理論上只要反射的接口和 goid 成員的名字不發(fā)生變化,代碼都可以正常運(yùn)行。經(jīng)過(guò)實(shí)際測(cè)試,以上的代碼可以在 Go1.8、Go1.9 和 Go1.10 版本中正確運(yùn)行。樂(lè)觀推測(cè),如果 g 結(jié)構(gòu)體類(lèi)型的名字不發(fā)生變化,Go 語(yǔ)言反射的機(jī)制也不發(fā)生變化,那么未來(lái) Go 語(yǔ)言版本應(yīng)該也是可以運(yùn)行的。
反射雖然具備一定的靈活性,但是反射的性能一直是被大家詬病的地方。一個(gè)改進(jìn)的思路是通過(guò)反射獲取 goid 的偏移量,然后通過(guò) g 指針和偏移量獲取 goid,這樣反射只需要在初始化階段執(zhí)行一次。
下面是 g_goid_offset 變量的初始化代碼:
var g_goid_offset uintptr = func() uintptr {
g := GetGroutine()
if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {
return f.Offset
}
panic("can not find g.goid field")
}()
有了正確的 goid 偏移量之后,采用前面講過(guò)的方式獲取 goid:
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
至此我們獲取 goid 的實(shí)現(xiàn)思路已經(jīng)足夠完善了,不過(guò)匯編的代碼依然有嚴(yán)重的安全隱患。
雖然 getg 函數(shù)是用 NOSPLIT 標(biāo)志聲明的禁止棧分裂的函數(shù)類(lèi)型,但是 getg 內(nèi)部又調(diào)用了更為復(fù)雜的 runtime·convT2E 函數(shù)。runtime·convT2E 函數(shù)如果遇到??臻g不足,可能觸發(fā)棧分裂的操作。而棧分裂時(shí),GC 將要挪動(dòng)棧上所有函數(shù)的參數(shù)和返回值和局部變量中的棧指針。但是我們的 getg 函數(shù)并沒(méi)有提供局部變量的指針信息。
下面是改進(jìn)后的 getg 函數(shù)的完整實(shí)現(xiàn):
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
NO_LOCAL_POINTERS
MOVQ $0, ret_type+0(FP)
MOVQ $0, ret_data+8(FP)
GO_RESULTS_INITIALIZED
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret_type+0(FP)
MOVQ BX, ret_data+8(FP)
RET
其中 NO_LOCAL_POINTERS 表示函數(shù)沒(méi)有局部指針變量。同時(shí)對(duì)返回的接口進(jìn)行零值初始化,初始化完成后通過(guò) GO_RESULTS_INITIALIZED 告知 GC。這樣可以在保證棧分裂時(shí),GC 能夠正確處理返回值和局部變量中的指針。
有了 goid 之后,構(gòu)造 Goroutine 局部存儲(chǔ)就非常容易了。我們可以定義一個(gè) gls 包提供 goid 的特性:
package gls
var gls struct {
m map[int64]map[interface{}]interface{}
sync.Mutex
}
func init() {
gls.m = make(map[int64]map[interface{}]interface{})
}
gls 包變量簡(jiǎn)單包裝了 map,同時(shí)通過(guò) sync.Mutex
互斥量支持并發(fā)訪(fǎng)問(wèn)。
然后定義一個(gè) getMap 內(nèi)部函數(shù),用于獲取每個(gè) Goroutine 字節(jié)的 map:
func getMap() map[interface{}]interface{} {
gls.Lock()
defer gls.Unlock()
goid := GetGoid()
if m, _ := gls.m[goid]; m != nil {
return m
}
m := make(map[interface{}]interface{})
gls.m[goid] = m
return m
}
獲取到 Goroutine 私有的 map 之后,就是正常的增、刪、改操作接口了:
func Get(key interface{}) interface{} {
return getMap()[key]
}
func Put(key interface{}, v interface{}) {
getMap()[key] = v
}
func Delete(key interface{}) {
delete(getMap(), key)
}
最后我們?cè)偬峁┮粋€(gè) Clean 函數(shù),用于釋放 Goroutine 對(duì)應(yīng)的 map 資源:
func Clean() {
gls.Lock()
defer gls.Unlock()
delete(gls.m, GetGoid())
}
這樣一個(gè)極簡(jiǎn)的 Goroutine 局部存儲(chǔ) gls 對(duì)象就完成了。
下面是使用局部存儲(chǔ)簡(jiǎn)單的例子:
import (
gls "path/to/gls"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
defer gls.Clean()
defer func() {
fmt.Printf("%d: number = %d\n", idx, gls.Get("number"))
}()
gls.Put("number", idx+100)
}(i)
}
wg.Wait()
}
通過(guò) Goroutine 局部存儲(chǔ),不同層次函數(shù)之間可以共享存儲(chǔ)資源。同時(shí)為了避免資源泄漏,需要在 Goroutine 的根函數(shù)中,通過(guò) defer 語(yǔ)句調(diào)用 gls.Clean() 函數(shù)釋放資源。
![]() | ![]() |
更多建議: