Maison > Article > développement back-end > Pourquoi Go est-il si « rapide » ?
Cet article présente principalement l'architecture d'implémentation du planificateur interne du programme Go (modèle G-P-M) afin d'obtenir des performances de concurrence extrêmement élevées, et comment le planificateur Go gère les scénarios de blocage de threads afin de maximiser l'utilisation des ressources informatiques.
Comment rendre notre système plus rapide
Avec le développement rapide des technologies de l'information, la puissance de traitement d'un seul serveur devient de plus en plus forte, obligeant le modèle de programmation à le changement par rapport au mode de ligne série précédent est mis à niveau vers le modèle simultané.
Les modèles de concurrence incluent le multiplexage d'E/S, le multi-processus et le multi-threading. Chacun de ces modèles a ses propres avantages et inconvénients. La plupart des architectures complexes modernes à haute concurrence utilisent plusieurs modèles ensemble, et différents modèles sont utilisés. dans différents scénarios, utilisez les points forts et évitez les faiblesses pour maximiser les performances du serveur.
Le multithreading, en raison de sa légèreté et de sa facilité d'utilisation, est devenu le modèle de concurrence le plus fréquemment utilisé dans la programmation concurrente, y compris les coroutines dérivées ultérieurement et d'autres sous-produits, qui sont également basés sur celui-ci.
Concurrence ≠ Parallèle
La concurrence et le parallélisme sont différents.
Sur un seul cœur de processeur, les threads réalisent la commutation des tâches à travers des tranches de temps ou abandonnent les droits de contrôle pour atteindre l'objectif d'exécuter plusieurs tâches "en même temps". Mais en fait, une seule tâche est exécutée à la fois et les autres tâches sont mises en file d’attente via un algorithme.
Le processeur multicœur permet à "plusieurs threads" dans le même processus de s'exécuter en même temps dans le vrai sens du terme.
Processus, thread, coroutine
Processus : Le processus est l'unité de base d'allocation de ressources dans le système et dispose d'un espace mémoire indépendant.
Thread : le thread est l'unité de base de la planification et de la répartition du processeur. Les threads sont attachés aux processus, et chaque thread partagera les ressources du processus parent.
Coroutine : Coroutine est un thread léger en mode utilisateur. La planification de la coroutine est entièrement contrôlée par l'utilisateur. Le basculement entre les coroutines nécessite uniquement la sauvegarde du contexte de la tâche, sans surcharge du noyau.
Changement de contexte de thread
En raison du traitement des interruptions, du multitâche, du changement de mode utilisateur et d'autres raisons, le processeur passe d'un thread à un autre et le processus de commutation doit être enregistré. L'état du processus en cours et restaurer l'état d'un autre processus.
Le changement de contexte coûte cher car il faut beaucoup de temps pour échanger les threads sur le noyau. La latence du changement de contexte dépend de différents facteurs et peut aller de 50 à 100 nanosecondes. Considérant que le matériel exécute en moyenne 12 instructions par nanoseconde et par cœur, un changement de contexte peut coûter entre 600 et 1 200 instructions en latence. En fait, le changement de contexte prend beaucoup de temps au programme pour exécuter les instructions.
S'il existe un changement de contexte entre cœurs, cela peut rendre le cache du processeur invalide (le coût du processeur pour accéder aux données du cache est d'environ 3 à 40 cycles d'horloge, et le coût d'accès les données de la mémoire principale sont d'environ 100 à 300 cycles d'horloge), le coût de commutation dans ce scénario sera plus élevé.
Golang est né pour la concurrence
Depuis sa sortie officielle en 2009, Golang a rapidement occupé des parts de marché en raison de sa vitesse d'exécution extrêmement élevée et de son efficacité de développement efficace. Golang prend en charge la concurrence au niveau du langage et utilise la coroutine légère Goroutine pour réaliser l'exécution simultanée de programmes.
Goroutine est très léger, cela se reflète principalement dans les deux aspects suivants :
Le coût du changement de contexte est faible : le changement de contexte Goroutine n'implique que la modification de la valeur de trois registres (PC / SP / DX) ; En revanche, le changement de contexte des threads nécessite un changement de mode (passage du mode utilisateur au mode noyau) et un rafraîchissement de 16 registres, PC, SP... et autres registres
Faible utilisation de la mémoire : l'espace de la pile des threads est ; généralement 2 Mo, l'espace minimum de la pile Goroutine est de 2 Ko ;
Le programme Golang peut facilement prendre en charge le fonctionnement Goroutine de niveau 10 W, et lorsque le nombre de threads atteint 1 Ko, l'utilisation de la mémoire a atteint 2 Go.
Mécanisme d'implémentation du planificateur Go :
Le programme Go utilise le planificateur pour planifier l'exécution de Goroutine sur le thread du noyau, mais Goroutine n'est pas directement lié au thread du système d'exploitation M- Machine Au lieu de cela, le processeur P (processeur logique) du planificateur Goroutine agit comme un « intermédiaire » pour obtenir les ressources du thread du noyau.
Le modèle du planificateur Go est généralement appelé modèle G-P-M. Il comprend 4 structures importantes, à savoir G, P, M et Sched :
G : Chaque Goroutine correspond à une structure G. . Body, G stocke la pile en cours d'exécution, les fonctions d'état et de tâche, qui peuvent être réutilisées.
G n'est pas un corps d'exécution. Chaque G doit être lié à P pour être programmé pour l'exécution.
P : Processeur, qui représente un processeur logique. Pour G, P équivaut à un cœur de CPU G ne peut être planifié que s'il est lié à P. Pour M, P fournit un environnement d'exécution pertinent (Contexte), tel que l'état d'allocation de mémoire (mcache), la file d'attente des tâches (G), etc.
Le nombre de P détermine le nombre maximum de G pouvant être parallélisés dans le système (prémisse : le nombre de cœurs physiques du CPU >= le nombre de P).
Le nombre de P est déterminé par le GoMAXPROCS défini par l'utilisateur, mais quelle que soit la taille du paramètre GoMAXPROCS, le nombre maximum de P est de 256.
M : Machine, abstraction du thread du noyau du système d'exploitation, représente la ressource qui effectue réellement les calculs. Après avoir lié un P valide, elle entre dans la boucle de planification et le mécanisme de la boucle de planification provient à peu près de la file d'attente globale, de la file d'attente locale et de la file d'attente locale de P ; file d'attente obtenue. Le nombre de
M est variable et est ajusté par Go Runtime. Afin d'éviter que le système ne planifie trop de threads de système d'exploitation en raison de la création d'un trop grand nombre, la limite maximale par défaut est de 10 000.
M ne conserve pas l'état de G, qui constitue la base de la planification de G dans M.
Sched : planificateur Go, qui maintient des files d'attente qui stockent M et G et certaines informations d'état du planificateur.
Le mécanisme de boucle du planificateur consiste grossièrement à obtenir G à partir de diverses files d'attente et de la file d'attente locale de P, à passer à la pile d'exécution de G et à exécuter la fonction de G, à appeler Goexit pour nettoyer et revenir à M, donc à plusieurs reprises.
Pour comprendre la relation entre M, P et G, vous pouvez illustrer la relation à travers le modèle classique d'un chariot à gopher déplaçant des briques :
Le Gopher's Le travail est le suivant : il y a un certain nombre de briques sur le chantier et le Gopher utilise un chariot pour transporter les briques jusqu'à l'amadou pour la cuisson. M peut être considéré comme le gopher sur la photo, P est la voiture et G est les briques installées dans la voiture.
Maintenant que nous avons compris la relation entre eux trois, commençons à nous concentrer sur la façon dont le gopher transporte les briques.
Processeur (P) :
Créez un lot de voitures (P) en fonction de la valeur GoMAXPROCS définie par l'utilisateur.
Goroutine(G) :
Le mot-clé Go est utilisé pour créer une Goroutine, ce qui équivaut à fabriquer une brique (G), puis à placer cette brique (G) dans le courant This la voiture est en (P).
Machine (M) :
La taupe (M) ne peut pas être créée en externe. Il y a trop de briques (G) et trop peu de taupes (M). Cependant, elle est vraiment occupée. S'il y a une voiture gratuite (P) qui n'est pas utilisée, empruntez d'autres gophers (M) ailleurs jusqu'à ce que toutes les voitures (P) soient épuisées.
Voici un processus où la taupe (M) ne suffit pas et la taupe (M) est empruntée ailleurs. Ce processus consiste à créer un thread noyau (M).
Il est à noter que les gophers (M) ne peuvent pas transporter de briques sans chariots (P). Le nombre de chariots (P) détermine le nombre de gophers (M) qui peuvent travailler en Go. Le nombre correspondant dans le. le programme est le nombre de threads actifs ;
Dans le programme Go, nous affichons le modèle G-P-M à travers l'illustration suivante :
P signifie "parallèle" "Le processeur logique en cours d'exécution, chaque P est affecté à un thread système M, G représente la coroutine Go.
Il existe deux files d'attente d'exécution différentes dans le planificateur Go : la file d'attente d'exécution globale (GRQ) et la file d'attente d'exécution locale (LRQ).
Chaque P a un LRQ, qui est utilisé pour gérer les Goroutines assignées à s'exécuter dans le contexte de P. Ces Goroutines sont à tour de rôle commutées en contexte par le M lié à P. GRQ s'applique aux Goroutines non encore affectées à P .
Comme le montre la figure ci-dessus, le nombre de G peut être bien supérieur au nombre de M. En d'autres termes, le programme Go peut utiliser un petit nombre de threads au niveau du noyau pour prendre en charge la concurrence. d'un grand nombre de Goroutines. Plusieurs Goroutines partagent les ressources informatiques du thread du noyau M via un changement de contexte au niveau de l'utilisateur, mais il n'y a aucune perte de performances causée par le changement de contexte de thread pour le système d'exploitation.
Afin d'utiliser pleinement les ressources informatiques des threads, le planificateur Go adopte les stratégies de planification suivantes :
Vol de tâches (vol de travail)
Nous savons que la réalité est Certains Goroutines fonctionnent vite, et certains fonctionnent lentement, ce qui entraînera certainement le problème d'être occupé à mort et inactif à mort. Go ne permet certainement pas l'existence de la pêche P, et il est tenu d'utiliser pleinement les ressources informatiques. .
Afin d'améliorer les capacités de traitement parallèle de Go et d'augmenter l'efficacité globale du traitement, lorsque les tâches G entre chaque P sont déséquilibrées, le planificateur permet d'obtenir l'exécution de G à partir du GRQ ou du LRQ d'autres P.
Réduire le blocage
Et si le Goroutine en cours d'exécution bloque le thread M ? Goroutine en LRQ sur P ne pourra-t-il pas obtenir de planning ?
Le blocage dans Go est principalement divisé en 4 scénarios suivants :
Scénario 1 : Goroutine est bloqué en raison d'appels atomiques, mutex ou d'opération de canal, et le planificateur changera le Goroutine Go actuellement bloqué sortir et reprogrammer d'autres Goroutines sur LRQ ;
Scénario 2 : Goroutine est bloqué en raison de requêtes réseau et d'opérations IO Dans ce cas de blocage, que feront nos G et M ?
Le programme Go fournit un observateur de réseau (NetPoller) pour gérer les requêtes réseau et les opérations d'E/S. Son arrière-plan implémente le multiplexage d'E/S via kqueue (MacOS), epoll (Linux) ou iocp (Windows).
En utilisant NetPoller pour passer des appels système réseau, le planificateur peut empêcher Goroutine de bloquer M lors de ces appels système. Cela permet à M d'exécuter d'autres Goroutines dans le LRQ de P sans créer un nouveau M. Aide à réduire la charge de planification sur le système d’exploitation.
La figure suivante montre comment cela fonctionne : G1 s'exécute sur M, et il y a 3 Goroutines en attente d'exécution sur LRQ. L'interrogateur du réseau est inactif et ne fait rien.
Ensuite, G1 souhaite effectuer un appel système réseau, il est donc déplacé vers l'observateur réseau et gère l'appel système réseau asynchrone. M peut alors exécuter des Goroutines supplémentaires à partir du LRQ. À ce stade, G2 est le contexte commuté sur M.
Enfin, l'appel système réseau asynchrone est complété par l'interrogateur réseau et G1 est replacé vers le LRQ de P. Une fois que G1 peut basculer le contexte sur M, le code lié à Go dont il est responsable peut être à nouveau exécuté. Le gros avantage ici est qu’aucun M supplémentaire n’est requis pour effectuer des appels système réseau. L'interrogateur réseau utilise un thread système qui traite à tout moment une boucle d'événements active.
Cette méthode d'appel semble très compliquée. Heureusement, le langage Go cache cette "complexité" dans le Runtime : les développeurs Go n'ont pas besoin de faire attention à savoir si le socket est. non bloquant, et il n'est pas nécessaire d'enregistrer le rappel du descripteur de fichier en personne. Il vous suffit de traiter le socket dans la méthode "block I/O" dans la Goroutine correspondant à chaque connexion, réalisant ainsi une simple goroutine-per. -réseau de connexion. Mode de programmation (mais un grand nombre de Goroutines entraînera également des problèmes supplémentaires, tels qu'une augmentation de la mémoire de la pile et une charge accrue sur le planificateur).
Le "block socket" dans Goroutine vu par la couche utilisateur est en fait "simulé" par le netpoller dans le runtime Go via le mécanisme de multiplexage non-block socket + E/S. La bibliothèque nette de Go est implémentée exactement de cette façon.
Scénario 3 : lors de l'appel de certaines méthodes système, si la méthode système est bloquée lors de l'appel, dans ce cas, l'interrogeur réseau (NetPoller) ne peut pas être utilisé et la Goroutine qui effectue l'appel système bloquera le courant M.
Regardons la situation dans laquelle un appel système synchrone (tel qu'une E/S de fichier) entraînera le blocage de M : G1 effectuera un appel système synchrone pour bloquer M1.
Après l'intervention du planificateur : il reconnaît que G1 a provoqué le blocage de M1. À ce moment, le planificateur sépare M1 de P et enlève également G1. L'ordonnanceur introduit alors un nouveau M2 pour desservir P. À ce stade, G2 peut être sélectionné dans le LRQ et un changement de contexte peut être effectué sur M2.
Une fois l'appel système de blocage terminé : G1 peut être replacé vers LRQ et exécuté à nouveau par P. Si cela se reproduit, le M1 sera mis de côté pour une réutilisation future.
Scénario 4 : Si une opération de mise en veille est effectuée dans Goroutine, M sera bloqué.
Le programme Go dispose d'un système de thread de surveillance en arrière-plan, qui surveille les tâches G de longue durée et définit les identifiants qui peuvent être usurpés, afin que d'autres Goroutines puissent les exécuter de manière préventive.
Tant que cette Goroutine fera un appel de fonction la prochaine fois, elle sera occupée, la scène sera également protégée, puis remise dans la file d'attente locale de P pour attendre la prochaine exécution.
Résumé
Cet article présente principalement le modèle G-P-M du point de vue de l'architecture du planificateur Go, et comment utiliser ce modèle pour obtenir un petit nombre de threads du noyau à prendre en charge le fonctionnement simultané d'un grand nombre de Goroutines. Et grâce à NetPoller, sysmon, etc., il aide les programmes Go à réduire le blocage des threads et à utiliser pleinement les ressources informatiques existantes, maximisant ainsi l'efficacité opérationnelle des programmes Go.
Pour plus de connaissances sur le langage Go, veuillez prêter attention à la colonne Tutoriel du langage Go sur le site Web PHP chinois.
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!