Golang 獲取 goroutine id 完全指南
在Golang中,每個goroutine協程都有一個goroutine id (goid),該goid沒有嚮應用層暴露。但是,在很多場景下,開發者又希望使用goid作為唯一標識,將一個goroutine中的函式層級呼叫串聯起來。比如,希望在一個http handler中將這個請求的每行日誌都加上對應的goid以便於對這個請求處理過程進行跟蹤和分析。
關於是否應該將goid暴露給應用層已經爭論多年。基本上,Golang的開發者都一致認為不應該暴露goid(faq: document why there is no way to get a goroutine ID),主要有以下幾點理由:
- goroutine設計理念是輕量,鼓勵開發者使用多goroutine進行開發,不希望開發者通過goid做goroutine local storage或thread local storage(TLS)的事情;
- Golang開發者Brad認為TLS在C/C++實踐中也問題多多,比如一些使用TLS的庫,thread狀態非常容易被非期望執行緒修改,導致crash.
- goroutine並不等價於thread, 開發者可以通過syscall獲取thread id,因此根本不需要暴露goid.
官方也一直推薦使用context作為上下文關聯的最佳實踐。如果你還是想獲取goid,下面是我整理的目前已知的所有獲取它的方式,希望你想清楚了再使用。
- 通過stack資訊獲取goroutine id.
- 通過修改原始碼獲取goroutine id.
- 通過CGo獲取goroutine id.
- 通過彙編獲取goroutine id.
- 通過彙編獲取偽goroutine id.
在開始介紹各種方法前,先看一下定義在src/runtime/runtime2.go
中儲存goroutine狀態的g
結構:
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64 // goroutine id
...
其中goid int64
欄位即為當前goroutine的id。
1. 通過stack資訊獲取goroutine id
package main
import (
"bytes"
"fmt"
"runtime"
"strconv"
)
func main() {
fmt.Println(GetGID())
}
func GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
原理非常簡單,將stack中的文字資訊"goroutine 1234"匹配出來。但是這種方式有兩個問題:
- stack資訊的格式隨版本更新可能變化,甚至不再提供goroutine id,可靠性差。
- 效能較差,呼叫10000次消耗>50ms。
如果你只是想在個人專案中使用goid,這個方法是可以勝任的。維護和修改成本相對較低,且不需要引入任何第三方依賴。同時建議你就此打住,不要繼續往下看了。
2. 通過修改原始碼獲取goroutine id
既然方法1效率較低,且不可靠,那麼我們可以嘗試直接修改原始碼src/runtime/runtime2.go
中新增Goid
函式,將goid暴露給應用層:
func Goid() int64 {
_g_ := getg()
return _g_.goid
}
這個方式能解決法1的兩個問題,但是會導致你的程式只能在修改了原始碼的機器上才能編譯,沒有移植性,並且每次go版本升級以後,都需要重新修改原始碼,維護成本較高。
3. 通過CGo獲取goroutine id
那麼有沒有效能好,同時不影響移植性,且維護成本低的方法呢?那就是來自Dave Cheney的CGo方式:
檔案id.c:
#include "runtime.h"
int64 ·Id(void) {
return g->goid;
}
檔案id.go:
package id
func Id() int64
完整程式碼參見junk/id.
這種方法的問題在於你需要開啟CGo, CGo存在一些缺點,具體可以參見這個大牛的cgo is not Go. 我相信在你絕大部分的工程專案中,你是不希望開啟CGo的。
4. 通過彙編獲取goroutine id
如果前面三種方法我們都不能接受,有沒有第四種方法呢?那就是通過彙編獲取goroutine id的方法。原理是:通過getg方法(彙編實現)獲取到當前goroutine的g結構地址,根據偏移量計算出成員goid int
的地址,然後取出該值即可。
專案goroutine實現了這種方法。需要說明的是,這種方法看似簡單,實際上因為每個go版本幾乎都會有針對g結構的調整,因此goid int64
的偏移並不是固定的,更加複雜的是,go在編譯的時候,傳遞的編譯引數也會影響goid int64
的偏移值,因此,這個專案的作者花了非常多精力來維護每個go版本g結構偏移的計算,詳見hack目錄。
這個方法效能好,原理清晰,實際使用上穩定性也不錯(我們在部分不太重要的線上業務使用了這種方法)。但是,維護這個庫也許真的太累了,最近發現作者將這個庫標記為“DEPRECATED”,看來獲取goroutine id是條越走越遠的不歸路
5. 通過彙編獲取偽goroutine id
雖然方法4從原理和實際應用上表現都不錯,但是畢竟作者棄坑了。回到我們要解決的問題上:我們並不是真的一定要獲取到goroutine id,我們只是想獲取到goroutine的唯一標識。那麼,從這個角度看的話,我們只需要解決goroutine標識唯一性的問題即可。
顯然,上面作者也想清楚了這個問題。他新開了一個庫go-tls, 這個庫實現了goroutine local storage,其中獲取goroutine id的方式是:用方法4的彙編獲取goroutine的地址,然後自己管理和分配goroutine id。由於它獲取到的並不是真正的goroutine id,因此我將之稱為偽goroutine id。其實現的核心程式碼如下:
var (
tlsDataMap = map[unsafe.Pointer]*tlsData{}
tlsMu sync.Mutex
tlsUniqueID int64
)
...
func fetchDataMap(readonly bool) *tlsData {
gp := g.G() // 1. 獲取g結構地址
if gp == nil {
return nil
}
// Try to find saved data.
needHack := false
tlsMu.Lock()
dm := tlsDataMap[gp]
if dm == nil && !readonly {
needHack = true
dm = &tlsData{
id: atomic.AddInt64(&tlsUniqueID, 1), // 2. 分配偽goroutine id
data: dataMap{},
}
tlsDataMap[gp] = dm
}
tlsMu.Unlock()
// Current goroutine is not hacked. Hack it.
if needHack {
if !hack(gp) {
tlsMu.Lock()
delete(tlsDataMap, gp)
tlsMu.Unlock()
}
}
return dm
}
- 獲取g結構地址。
- 分配偽goroutine id.
這種方式基本沒有什麼不能接受的hack實現,從原理上來說也更加安全。但是獲取到不是你最開始想要的goroutine id,不知你能否接受
小結
獲取goroutine id是一條不歸路,目前也沒有完美的獲取它的方式。如果你一定要使用goroutine id,先想清楚你要解決的問題是什麼,如果沒有必要,建議你不要走上這條不歸路。儘早在團隊中推廣使用context, 越早使用越早脫離對goroutine id的留戀和掙扎。
Credit
Originally published at liudanking.com
相關文章
- [Golang基礎]GoroutineGolang
- 在 JDBC 中獲取插入 IDJDBC
- 獲取gridview所有行的idView
- function ALV 獲取OO ALV event IDFunction
- 關於golang的goroutine schedulerGolang
- golang 介面按需獲取資源Golang
- 分散式雪花演算法獲取id分散式演算法
- java 獲取當前程式的程式IDJava
- Golang —— goroutine(協程)和channel(管道)Golang
- Android 通過名稱獲取資源IDAndroid
- Golang 的 goroutine 是如何實現的?Golang
- Golang 入門 : 等待 goroutine 完成任務Golang
- 「Golang成長之路」併發之GoroutineGolang
- 根據id獲取元素的寬度的方法
- mysql獲取指定表當前自增id值MySql
- 2020年使用者獲取指南
- Golang併發程式設計——goroutine、channel、syncGolang程式設計
- 第 12 期 golang 中 goroutine 的排程Golang
- 說說Golang goroutine併發那些事兒Golang
- Mysql在資料插入後立即獲取插入的IdMySql
- 拼多多也可以透過ID獲取商品詳情?
- Flutter獲取IOS bundle id和Android應用包名FlutteriOSAndroid
- activiti 根據 流程例項ID 獲取發起人
- Golang語言並行設計的核心goroutineGolang並行
- golang 利用 WaitGroup 控制多個 goroutine 同時完成GolangAI
- 利用mitmproxy實現抖音Cookie,裝置ID獲取(一)MITCookie
- 利用 mitmproxy 實現抖音 Cookie,裝置 ID 獲取 (一)MITCookie
- SpringData 完全入門指南Spring
- 精讀《useEffect 完全指南》
- golang gopsutil 程式 系統硬體資訊 獲取Golang
- Spring MVC 入門指南(十三):獲取Cookie值SpringMVCCookie
- golang pprof 監控系列(4) —— goroutine thread 統計原理Golangthread
- Golang-goroutine02(MPG模式+設定CPU數目)Golang模式
- Golang雜談-gorm整合雪花idGolangORM
- “Emacs 遊戲機”完全指南Mac遊戲
- 容器快速入門完全指南
- Conflux Truffle 使用完全指南UX
- 【VSC】Snippets不完全指南