Dans un système d'exploitation, chaque processus a un ID de processus unique et chaque thread a son propre ID de thread unique. De même, dans le langage Go, chaque Goroutine possède son propre identifiant de routine Go unique, qui est souvent rencontré dans des scénarios comme la panique. Bien que les Goroutines aient des identifiants inhérents, le langage Go ne fournit délibérément pas d'interface pour obtenir cet identifiant. Cette fois, nous tenterons d'obtenir l'identifiant Goroutine via le langage assembleur Go.
Selon les documents officiels pertinents, la raison pour laquelle le langage Go ne fournit délibérément pas de goid est pour éviter les abus. Parce que la plupart des utilisateurs, après avoir facilement obtenu le goid, écriront inconsciemment du code qui dépend fortement de goid dans la programmation ultérieure. Une forte dépendance à l'égard de goid rendra ce code difficile à porter et compliquera également le modèle concurrent. Dans le même temps, il peut y avoir un grand nombre de Goroutines dans le langage Go, mais il n'est pas facile de surveiller en temps réel quand chaque Goroutine est détruite, ce qui empêchera également le recyclage automatique des ressources qui dépendent de goid ( nécessitant un recyclage manuel). Cependant, si vous êtes un utilisateur du langage assembleur Go, vous pouvez complètement ignorer ces problèmes.
Remarque : Si vous obtenez le goid de force, vous pourriez avoir "honte" ?:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
Pour faciliter la compréhension, essayons d'abord d'obtenir le goid en Go pur. Bien que les performances d'obtention du goid en Go pur soient relativement faibles, le code a une bonne portabilité et peut également être utilisé pour tester et vérifier si le goid obtenu par d'autres méthodes est correct.
Chaque utilisateur de la langue Go doit connaître la fonction panique. L’appel de la fonction panic provoquera une exception Goroutine. Si la panique n'est pas gérée par la fonction de récupération avant d'atteindre la fonction racine de Goroutine, le runtime imprimera les informations pertinentes sur l'exception et la pile et quittera Goroutine.
Construisons un exemple simple pour générer le goid par panique :
package main func main() { panic("leapcell") }
Après l'exécution, les informations suivantes seront affichées :
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
On peut deviner que le 1 dans la goroutine 1 [running] des informations de sortie Panic est le goid. Mais comment pouvons-nous obtenir les informations de sortie de panique dans le programme ? En fait, les informations ci-dessus ne sont qu'une description textuelle du cadre actuel de la pile d'appels de fonction. La fonction runtime.Stack fournit la fonction d'obtenir ces informations.
Reconstruisons un exemple basé sur la fonction runtime.Stack pour afficher le goid en affichant les informations du frame de pile actuel :
package main func main() { panic("leapcell") }
Après l'exécution, les informations suivantes seront affichées :
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
Ainsi, il est facile d'analyser les informations goid à partir de la chaîne obtenue par runtime.Stack :
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
Nous ne détaillerons pas les détails de la fonction GetGoid. Il convient de noter que la fonction runtime.Stack peut non seulement obtenir les informations de pile du Goroutine actuel mais également les informations de pile de tous les Goroutines (contrôlées par le deuxième paramètre). Dans le même temps, la fonction net/http2.curGoroutineID du langage Go obtient le goid de la même manière.
Selon la documentation officielle du langage assembleur Go, le pointeur g de chaque structure Goroutine en cours d'exécution est stocké dans le stockage local TLS du thread système où se trouve le Goroutine en cours d'exécution. Nous pouvons d'abord obtenir le stockage local du thread TLS, puis obtenir le pointeur de la structure g à partir du TLS, et enfin extraire le goid de la structure g.
Ce qui suit consiste à obtenir le pointeur g en se référant à la macro get_tls définie dans le package d'exécution :
goroutine 1 [running]: main.main() /path/to/main.g
Le get_tls est une fonction macro définie dans le fichier d'en-tête runtime/go_tls.h.
Pour la plateforme AMD64, la fonction macro get_tls est définie comme suit :
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) }
Après avoir développé la fonction macro get_tls, le code pour obtenir le pointeur g est le suivant :
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
En fait, TLS est similaire à l'adresse du stockage local du thread, et les données dans la mémoire correspondant à l'adresse sont le pointeur g. Nous pouvons être plus simples :
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
Sur la base de la méthode ci-dessus, nous pouvons envelopper une fonction getg pour obtenir le pointeur g :
MOVQ TLS, CX MOVQ 0(CX)(TLS*1), AX
Ensuite, dans le code Go, obtenez la valeur de goid grâce au décalage du membre goid dans la structure g :
MOVQ (TLS), AX
Ici, g_goid_offset est le décalage du membre goid. La structure g fait référence à runtime/runtime2.go.
Dans la version Go1.10, le décalage de goid est de 152 octets. Ainsi, le code ci-dessus ne peut s'exécuter correctement que dans les versions Go où le décalage goid est également de 152 octets. Selon l’oracle du grand Thompson, le dénombrement et la force brute sont la panacée à tous les problèmes difficiles. Nous pouvons également enregistrer les décalages goid dans un tableau, puis interroger le décalage goid en fonction du numéro de version Go.
Ce qui suit est le code amélioré :
// func getg() unsafe.Pointer TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10 func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }
Maintenant, le décalage goid peut enfin s'adapter automatiquement aux versions linguistiques Go publiées.
Bien que l'énumération et la force brute soient simples, elles ne prennent pas bien en charge les versions Go inédites en cours de développement. Nous ne pouvons pas connaître à l'avance le décalage du membre goid dans une certaine version en cours de développement.
S'il se trouve dans le package d'exécution, nous pouvons directement obtenir le décalage du membre via unsafe.OffsetOf(g.goid). Nous pouvons également obtenir le type de la structure g par réflexion, puis interroger le décalage d'un certain membre via le type. Étant donné que la structure g est un type interne, le code Go ne peut pas obtenir les informations de type de la structure g à partir de packages externes. Cependant, dans le langage assembleur Go, nous pouvons voir tous les symboles, donc théoriquement, nous pouvons également obtenir les informations de type de la structure g.
Une fois qu'un type est défini, le langage Go générera les informations de type correspondantes pour ce type. Par exemple, la structure g générera un identifiant type·runtime·g pour représenter les informations de type valeur de la structure g, ainsi qu'un identifiant type·*runtime·g pour représenter les informations de type pointeur. Si la structure g a des méthodes, alors les informations de type go.itab.runtime.g et go.itab.*runtime.g seront également générées pour représenter les informations de type avec des méthodes.
Si nous pouvons obtenir le type·runtime·g représentant le type de la structure g et le pointeur g, alors nous pouvons construire l'interface de l'objet g. Voici la fonction getg améliorée, qui renvoie l'interface de l'objet pointeur g :
package main func main() { panic("leapcell") }
Ici, le registre AX correspond au pointeur g, et le registre BX correspond au type de la structure g. Ensuite, la fonction runtime·convT2E est utilisée pour convertir le type en interface. Parce que nous n’utilisons pas le type pointeur de la structure g, l’interface renvoyée représente le type valeur de la structure g. Théoriquement, nous pouvons également construire une interface de type pointeur g, mais en raison des limitations du langage assembleur Go, nous ne pouvons pas utiliser l'identifiant type·*runtime·g.
Basé sur l'interface renvoyée par g, il est facile d'obtenir le goid :
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
Le code ci-dessus obtient directement le goid par réflexion. Théoriquement, tant que le nom de l'interface réfléchie et le membre goid ne changent pas, le code peut s'exécuter normalement. Après des tests réels, le code ci-dessus peut s'exécuter correctement dans les versions Go1.8, Go1.9 et Go1.10. Avec un certain optimisme, si le nom du type de structure g ne change pas et que le mécanisme de réflexion du langage Go ne change pas, il devrait également pouvoir s'exécuter dans les futures versions du langage Go.
Bien que la réflexion ait un certain degré de flexibilité, la performance de la réflexion a toujours été critiquée. Une idée améliorée consiste à obtenir le décalage du goid par réflexion, puis à obtenir le goid via le pointeur g et le décalage, de sorte que la réflexion ne doive être exécutée qu'une seule fois dans la phase d'initialisation.
Ce qui suit est le code d'initialisation de la variable g_goid_offset :
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
Après avoir obtenu le bon décalage de goid, obtenez le goid de la manière mentionnée précédemment :
package main func main() { panic("leapcell") }
À ce stade, notre idée d'implémentation pour obtenir le goid est suffisamment complète, mais le code assembleur présente encore de sérieux risques de sécurité.
Bien que la fonction getg soit déclarée comme un type de fonction qui interdit le fractionnement de pile avec l'indicateur NOSPLIT, la fonction getg appelle en interne la fonction runtime·convT2E plus complexe. Si la fonction runtime·convT2E rencontre un espace de pile insuffisant, elle peut déclencher des opérations de fractionnement de pile. Lorsque la pile est divisée, le GC déplacera les pointeurs de pile dans les paramètres de fonction, les valeurs de retour et les variables locales. Cependant, notre fonction getg ne fournit pas d'informations de pointeur pour les variables locales.
Ce qui suit est l'implémentation complète de la fonction getg améliorée :
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
Ici, NO_LOCAL_POINTERS signifie que la fonction n'a pas de variables de pointeur local. Dans le même temps, l'interface renvoyée est initialisée avec des valeurs nulles et une fois l'initialisation terminée, GO_RESULTS_INITIALIZED est utilisé pour informer le GC. Cela garantit que lorsque la pile est divisée, le GC peut gérer correctement les pointeurs dans les valeurs de retour et les variables locales.
Avec le goid, il est très simple de construire un stockage local Goroutine. Nous pouvons définir un package gls pour fournir la fonctionnalité goid :
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
La variable du package gls enveloppe simplement une carte et prend en charge l'accès simultané via le mutex sync.Mutex.
Définissez ensuite une fonction interne getMap pour obtenir la carte pour chaque octet Goroutine :
goroutine 1 [running]: main.main() /path/to/main.g
Après avoir obtenu la carte privée de la Goroutine, c'est l'interface normale pour les opérations d'ajout, de suppression et de modification :
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) }
Enfin, nous fournissons une fonction Clean pour libérer les ressources cartographiques correspondant à la Goroutine :
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
De cette façon, un objet gls de stockage local Goroutine minimaliste est complété.
Ce qui suit est un exemple simple d'utilisation du stockage local :
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
Grâce au stockage local Goroutine, différents niveaux de fonctions peuvent partager des ressources de stockage. Dans le même temps, pour éviter les fuites de ressources, dans la fonction racine de Goroutine, la fonction gls.Clean() doit être appelée via l'instruction defer pour libérer les ressources.
Enfin, permettez-moi de vous recommander la plateforme la plus adaptée au déploiement des services Go : leapcell
Explorez-en davantage dans la documentation !
Twitter de Leapcell : https://x.com/LeapcellHQ
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!