Wie bekommt man die Goroutine ID?
Takashi Yamamoto
Infrastructure Engineer · Leapcell

In einem Betriebssystem hat jeder Prozess eine eindeutige Prozess-ID und jeder Thread hat seine eigene eindeutige Thread-ID. In ähnlicher Weise hat in der Go-Sprache jede Goroutine ihre eigene eindeutige Go-Routine-ID, die häufig in Szenarien wie Panic
auftritt. Obwohl Goroutinen inhärente IDs haben, bietet die Go-Sprache absichtlich keine Schnittstelle zum Abrufen dieser ID. Dieses Mal werden wir versuchen, die Goroutinen-ID durch die Go-Assembly-Sprache zu erhalten.
1. Das offizielle Design, keine goid
zu haben (https://github.com/golang/go/issues/22770)
Laut den offiziellen relevanten Materialien ist der Grund, warum die Go-Sprache absichtlich keine goid
bereitstellt, die Vermeidung von Missbrauch. Denn die meisten Benutzer werden, nachdem sie die goid
einfach erhalten haben, unbewusst Code schreiben, der in der nachfolgenden Programmierung stark von goid
abhängt. Eine starke Abhängigkeit von goid
erschwert das Portieren dieses Codes und verkompliziert auch das gleichzeitige Modell. Gleichzeitig kann es in der Go-Sprache eine große Anzahl von Goroutinen geben, aber es ist nicht einfach, in Echtzeit zu überwachen, wann jede Goroutine zerstört wird, was auch dazu führt, dass Ressourcen, die von goid
abhängen, nicht automatisch recycelt werden (manuelle Wiederverwertung erforderlich). Wenn Sie jedoch ein Go-Assembly-Sprachenbenutzer sind, können Sie diese Bedenken vollständig ignorieren.
Hinweis: Wenn Sie die goid
erzwingen, werden Sie möglicherweise "beschämt" 😂:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
2. Abrufen von goid
in reinem Go
Um das Verständnis zu erleichtern, versuchen wir zunächst, die goid
in reinem Go abzurufen. Obwohl die Leistung des Abrufens der goid
in reinem Go relativ gering ist, hat der Code eine gute Portabilität und kann auch zum Testen und Überprüfen verwendet werden, ob die durch andere Methoden abgerufene goid
korrekt ist.
Jeder Go-Sprachenbenutzer sollte die Funktion panic
kennen. Das Aufrufen der Funktion panic
verursacht eine Goroutine-Ausnahme. Wenn die panic
nicht von der Funktion recover
behandelt wird, bevor sie die Stammfunktion der Goroutine erreicht, gibt die Laufzeit relevante Ausnahme- und Stackinformationen aus und beendet die Goroutine.
Lassen Sie uns ein einfaches Beispiel erstellen, um die goid
durch panic
auszugeben:
package main func main() { panic("leapcell") }
Nach dem Ausführen werden die folgenden Informationen ausgegeben:
panic: leapcell
goroutine 1 [running]:
main.main()
/path/to/main.go:4 +0x40
Wir können vermuten, dass die 1
in den Panic
-Ausgabeinformationen goroutine 1 [running]
die goid
ist. Aber wie können wir die panic
-Ausgabeinformationen im Programm abrufen? Tatsächlich sind die obigen Informationen nur eine textuelle Beschreibung des aktuellen Funktionsaufruf-Stackframes. Die Funktion runtime.Stack
bietet die Funktion zum Abrufen dieser Informationen.
Lassen Sie uns ein Beispiel basierend auf der Funktion runtime.Stack
rekonstruieren, um die goid
auszugeben, indem wir die Informationen des aktuellen Stackframes ausgeben:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
Nach dem Ausführen werden die folgenden Informationen ausgegeben:
goroutine 1 [running]:
main.main()
/path/to/main.g
Es ist also einfach, die goid
-Informationen aus der durch runtime.Stack
erhaltenen Zeichenfolge zu analysieren:
import ( "fmt" "strconv" "strings" "runtime" ) 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) }
Wir werden nicht auf die Details der Funktion GetGoid
eingehen. Es ist zu beachten, dass die Funktion runtime.Stack
nicht nur die Stackinformationen der aktuellen Goroutine abrufen kann, sondern auch die Stackinformationen aller Goroutinen (gesteuert durch den zweiten Parameter). Gleichzeitig ruft die Funktion net/http2.curGoroutineID
in der Go-Sprache die goid
auf ähnliche Weise ab.
3. Abrufen von goid
aus der g
-Struktur
Gemäss der offiziellen Go-Assembly-Sprache-Dokumentation wird der g
-Zeiger jeder laufenden Goroutinenstruktur im lokalen Speicher TLS des Systemthreads gespeichert, in dem sich die aktuelle laufende Goroutine befindet. Wir können zuerst den lokalen TLS-Thread-Speicher abrufen, dann den Zeiger der g
-Struktur aus dem TLS abrufen und schliesslich die goid
aus der g
-Struktur extrahieren.
Das Folgende dient dazu, den g
-Zeiger abzurufen, indem auf das im Paket runtime
definierte Makro get_tls
verwiesen wird:
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
Das get_tls
ist eine Makrofunktion, die in der Header-Datei runtime/go_tls.h
definiert ist.
Für die AMD64-Plattform ist die Makrofunktion get_tls
wie folgt definiert:
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
Nach dem Erweitern der Makrofunktion get_tls
sieht der Code zum Abrufen des g
-Zeigers wie folgt aus:
MOVQ TLS, CX MOVQ 0(CX)(TLS*1), AX
Tatsächlich ähnelt TLS der Adresse des lokalen Thread-Speichers, und die Daten im dem Address entsprechenden Speicher sind der g
-Zeiger. Wir können direkter sein:
MOVQ (TLS), AX
Basierend auf der obigen Methode können wir eine getg
-Funktion umschliessen, um den g
-Zeiger abzurufen:
// func getg() unsafe.Pointer TEXT ·getg(SB), NOSPLIT, $0-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET
Dann, im Go-Code, den Wert von goid
über den Offset des goid
-Members in der g
-Struktur abrufen:
const g_goid_offset = 152 // Go1.10 func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }
Hier ist g_goid_offset
der Offset des goid
-Members. Die g
-Struktur bezieht sich auf runtime/runtime2.go
.
In der Go1.10-Version beträgt der Offset von goid
152 Bytes. Daher kann der obige Code nur in Go-Versionen korrekt ausgeführt werden, in denen der goid
-Offset auch 152 Bytes beträgt. Laut dem Orakel des grossen Thompson sind Enumeration und Brute Force das Allheilmittel für alle schwierigen Probleme. Wir können auch die goid
-Offsets in einer Tabelle speichern und dann den goid
-Offset anhand der Go-Versionsnummer abfragen.
Das Folgende ist der verbesserte Code:
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) }()
Jetzt kann sich der goid
-Offset endlich automatisch an die veröffentlichten Go-Sprachversionen anpassen.
4. Abrufen des Schnittstellenobjekts, das der g
-Struktur entspricht
Obwohl Enumeration und Brute Force unkompliziert sind, unterstützen sie die unveröffentlichten Go-Versionen in der Entwicklung nicht gut. Wir können den Offset des goid
-Members in einer bestimmten Version in der Entwicklung nicht im Voraus ermitteln.
Wenn es sich innerhalb des Pakets runtime
befindet, können wir den Offset des Members direkt über unsafe.OffsetOf(g.goid)
abrufen. Wir können auch den Typ der g
-Struktur durch Reflexion abrufen und dann den Offset eines bestimmten Members über den Typ abfragen. Da die g
-Struktur ein interner Typ ist, kann Go-Code die Typinformationen der g
-Struktur nicht aus externen Paketen abrufen. In der Go-Assembly-Sprache können wir jedoch alle Symbole sehen, sodass wir theoretisch auch die Typinformationen der g
-Struktur abrufen können.
Nachdem ein Typ definiert wurde, generiert die Go-Sprache entsprechende Typinformationen für diesen Typ. Beispielsweise generiert die g
-Struktur einen type·runtime·g
-Bezeichner, um die Werttypinformationen der g
-Struktur darzustellen, und auch einen type·*runtime·g
-Bezeichner, um die Zeigertyinformationen darzustellen. Wenn die g
-Struktur Methoden hat, werden auch Typinformationen go.itab.runtime.g
und go.itab.*runtime.g
generiert, um die Typinformationen mit Methoden darzustellen.
Wenn wir das type·runtime·g
erhalten können, das den Typ der g
-Struktur darstellt, und den g
-Zeiger, können wir die Schnittstelle des g
-Objekts erstellen. Das Folgende ist die verbesserte getg
-Funktion, die die Schnittstelle des g
-Zeigerobjekts zurückgibt:
// 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
Hier entspricht das AX-Register dem g
-Zeiger und das BX-Register dem Typ der g
-Struktur. Anschliessend wird die Funktion runtime·convT2E
verwendet, um den Typ in eine Schnittstelle zu konvertieren. Da wir nicht den Zeigertyp der g
-Struktur verwenden, stellt die zurückgegebene Schnittstelle den Werttyp der g
-Struktur dar. Theoretisch können wir auch eine Schnittstelle des g
-Zeigertyps erstellen, aber aufgrund der Einschränkungen der Go-Assembly-Sprache können wir den Bezeichner type·*runtime·g
nicht verwenden.
Basierend auf der von g
zurückgegebenen Schnittstelle ist es einfach, die goid
abzurufen:
import ( "reflect" ) func GetGoid() int64 { g := getg() gid := reflect.ValueOf(g).FieldByName("goid").Int() return gid }
Der obige Code ruft die goid
direkt durch Reflexion ab. Theoretisch sollte der Code normal ausgeführt werden, solange sich der Name der reflektierten Schnittstelle und des goid
-Members nicht ändert. Nach tatsächlichen Tests kann der obige Code in den Go1.8-, Go1.9- und Go1.10-Versionen korrekt ausgeführt werden. Optimistisch gesehen sollte er auch in zukünftigen Go-Sprachversionen ausgeführt werden können, wenn sich der Name des g
-Strukturtyps und der Reflexionsmechanismus der Go-Sprache nicht ändern.
Obwohl die Reflexion ein gewisses Mass an Flexibilität aufweist, wurde die Leistung der Reflexion immer kritisiert. Eine verbesserte Idee ist, den Offset der goid
durch Reflexion abzurufen und dann die goid
über den g
-Zeiger und den Offset abzurufen, sodass die Reflexion nur einmal in der Initialisierungsphase ausgeführt werden muss.
Das Folgende ist der Initialisierungscode für die Variable 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") }()
Nachdem der korrekte goid
-Offset vorhanden ist, rufen Sie die goid
auf die zuvor erwähnte Weise ab:
func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }
An diesem Punkt ist unsere Implementierungsidee zum Abrufen der goid
vollständig genug, aber der Assembly-Code birgt immer noch ernsthafte Sicherheitsrisiken.
Obwohl die Funktion getg
als Funktionstyp deklariert ist, der das Stack-Splitting mit dem Flag NOSPLIT
verbietet, ruft die Funktion getg
intern die komplexere Funktion runtime·convT2E
auf. Wenn die Funktion runtime·convT2E
auf unzureichenden Stack-Speicherplatz stösst, kann dies Stack-Splitting-Operationen auslösen. Wenn der Stack aufgeteilt wird, verschiebt der GC die Stack-Zeiger in den Funktionsparametern, Rückgabewerten und lokalen Variablen. Unsere Funktion getg
liefert jedoch keine Zeigerinformationen für lokale Variablen.
Das Folgende ist die vollständige Implementierung der verbesserten Funktion getg
:
// 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
Hier bedeutet NO_LOCAL_POINTERS
, dass die Funktion keine lokalen Zeigervariablen hat. Gleichzeitig wird die zurückgegebene Schnittstelle mit Nullwerten initialisiert, und nachdem die Initialisierung abgeschlossen ist, wird GO_RESULTS_INITIALIZED
verwendet, um den GC zu informieren. Dies stellt sicher, dass der GC die Zeiger in den Rückgabewerten und lokalen Variablen korrekt verarbeiten kann, wenn der Stack aufgeteilt wird.
5. Anwendung von goid
: Lokaler Speicher
Mit der goid
ist es sehr einfach, einen lokalen Goroutine-Speicher zu erstellen. Wir können ein gls
-Paket definieren, um die goid
-Funktion bereitzustellen:
package gls var gls struct { m map[int64]map[interface{}]interface{} sync.Mutex } func init() { gls.m = make(map[int64]map[interface{}]interface{}}) }
Die Paketvariable gls
umschliesst einfach ein map
und unterstützt gleichzeitigen Zugriff über den Mutex sync.Mutex
.
Definieren Sie dann eine interne Funktion getMap
, um das map
für jedes Goroutine-Byte abzurufen:
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 }
Nachdem das private map
der Goroutine abgerufen wurde, ist es die normale Schnittstelle für Hinzufügungs-, Lösch- und Änderungsoperationen:
func Get(key interface{}) interface{} { return getMap()[key] } func Put(key interface{}, v interface{}) { getMap()[key] = v } func Delete(key interface{}) { delete(getMap(), key) }
Schliesslich stellen wir eine Funktion Clean
bereit, um die dem Goroutine entsprechenden map
-Ressourcen freizugeben:
func Clean() { gls.Lock() defer gls.Unlock() delete(gls.m, GetGoid()) }
Auf diese Weise wird ein minimalistisches lokales Goroutine-Speicherobjekt gls
vervollständigt.
Das Folgende ist ein einfaches Beispiel für die Verwendung des lokalen Speichers:
import ( "fmt" "sync" "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() }
Durch den lokalen Goroutine-Speicher können verschiedene Funktionsebenen Speicherressourcen gemeinsam nutzen. Um Ressourcenlecks zu vermeiden, muss in der Stammfunktion der Goroutine die Funktion gls.Clean()
über die Anweisung defer
aufgerufen werden, um Ressourcen freizugeben.
Leapcell: Die fortschrittliche Serverless-Plattform für das Hosten von Golang-Anwendungen
Zum Schluss möchte ich die am besten geeignete Plattform für die Bereitstellung von Go-Diensten empfehlen: leapcell
1. Unterstützung mehrerer Sprachen
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ