Maison > Article > développement back-end > Quelles sont les performances multithread de Python GIL ?
Avant-propos : les blogueurs entendent souvent le mot GIL lorsqu'ils entrent en contact pour la première fois avec Python, et constatent que ce mot est souvent assimilé à l'incapacité de Python à implémenter efficacement le multi-threading. Conformément à l'attitude de recherche consistant non seulement à savoir ce qui se passe, mais aussi pourquoi cela se produit, le blogueur a collecté toutes sortes d'informations, a passé quelques heures de temps libre en une semaine pour comprendre en profondeur GIL et les a résumées dans ceci J'espère également que les lecteurs pourront mieux et objectivement comprendre GIL à travers cet article.
Qu'est-ce que GIL ?
La première chose qui doit être claire est que
GIL
n'est pas une fonctionnalité de Python. Elle a été introduite lors de l'implémentation de Python. analyseur (CPython) un concept. Tout comme le C++ est un ensemble de normes de langage (grammaire), mais il peut être compilé en code exécutable à l'aide de différents compilateurs. Compilateurs célèbres tels que GCC, INTEL C++, Visual C++, etc. Il en va de même pour Python. Le même morceau de code peut être exécuté via différents environnements d'exécution Python tels que CPython, PyPy et Psyco. Par exemple, JPython n'a pas de GIL. Cependant, CPython est l'environnement d'exécution Python par défaut dans la plupart des environnements. Par conséquent, dans le concept de beaucoup de gens, CPython est Python, et ils tiennent pour acquis queGIL
est attribué aux défauts du langage Python. Alors soyons clairs ici : GIL n'est pas une fonctionnalité de Python. Python n'a pas du tout besoin de s'appuyer sur GILAlors, qu'est-ce que GIL dans l'implémentation de CPython ? Le nom complet de GIL
Global Interpreter Lock
Afin d'éviter toute tromperie, jetons un œil à l'explication officielle :Dans CPython, le verrouillage global de l'interprète, ou GIL, est un mutex qui empêche plusieurs natifs threads d'exécuter les bytecodes Python à la fois. Ce verrou est nécessaire principalement parce que la gestion de la mémoire de CPython n'est pas sécurisée pour les threads (cependant, depuis que le GIL existe, d'autres fonctionnalités dépendent des garanties qu'il applique.)
D'accord, ça n'a pas l'air mauvais ? Un Mutex qui empêche plusieurs threads d'exécuter du code machine simultanément apparaît comme un verrou global qui ressemble à un bug à première vue ! Ne vous inquiétez pas, nous l'analyserons lentement ci-dessous.
Pourquoi y a-t-il GIL
En raison de limitations physiques, la concurrence entre les fabricants de processeurs en termes de fréquence de cœur a été remplacée par le multicœur. Afin d'utiliser plus efficacement les performances des processeurs multicœurs, des méthodes de programmation multithread ont émergé, ce qui entraîne des difficultés de cohérence des données et de synchronisation du statut entre les threads. Même le Cache à l'intérieur du CPU ne fait pas exception. Afin de résoudre efficacement la synchronisation des données entre plusieurs cache, divers fabricants ont déployé beaucoup d'efforts, ce qui entraîne inévitablement certaines conséquences. perte.
Bien sûr, Python ne peut pas s'échapper. Afin de profiter de plusieurs cœurs, Python a commencé à prendre en charge le multi-threading. Le moyen le plus simple de résoudre l'intégrité des données et la synchronisation de l'état entre plusieurs threads est naturellement de verrouiller. Il y a donc le super gros verrou de GIL, et lorsque de plus en plus de développeurs de base de code acceptent ce paramètre, ils commencent à s'appuyer fortement sur cette fonctionnalité (c'est-à-dire que Python par défaut les objets internes Est thread-safe, pas besoin de considérer des verrous de mémoire supplémentaires et des opérations de synchronisation lors de la mise en œuvre).
Lentement, cette méthode de mise en œuvre s'est révélée douloureuse et inefficace. Mais lorsque tout le monde a essayé de diviser et de supprimer le GIL, ils ont constaté qu'un grand nombre de développeurs de codes de bibliothèque s'appuyaient fortement sur le GIL et qu'il était très difficile de le supprimer. Est-ce difficile ? Pour donner une analogie, un "petit projet" comme MySQL a mis près de 5 ans pour diviser le grand verrou de Buffer Pool Mutex en plusieurs petits verrous, de 5,5 à 5,6 à 5,7 ans et toujours en cours. MySQL, un produit bénéficiant du support de l'entreprise et d'une équipe de développement fixe derrière lui, traverse une période si difficile, sans parler d'une équipe hautement communautaire de développeurs principaux et de contributeurs de code comme Python ?
Donc pour faire simple, l'existence de GIL tient davantage à des raisons historiques. Si c’était à refaire, nous serions toujours confrontés au problème du multi-threading, mais au moins ce serait plus élégant que l’approche GIL actuelle.
L'impact de GIL
D'après l'introduction ci-dessus et la définition officielle, GIL est sans aucun doute un verrou exclusif mondial. Il ne fait aucun doute que l'existence de verrous globaux aura un grand impact sur l'efficacité du multi-threading. C'est presque comme si Python était un programme monothread. Les lecteurs diront alors que tant que le verrouillage global sera libéré, l'efficacité ne sera pas mauvaise. Tant que le GIL peut être libéré lors de l'exécution d'opérations d'E/S chronophages, l'efficacité opérationnelle peut encore être améliorée. En d'autres termes, peu importe à quel point c'est grave, ce ne sera pas pire que l'efficacité d'un seul thread. C'est vrai en théorie, mais en pratique ? Python est pire que vous ne le pensez.
Comparons l'efficacité de Python en multi-threading et en mono-threading. La méthode de test est très simple, un compteur fonction qui boucle 100 millions de fois. L’un est exécuté deux fois via un seul thread et l’autre est exécuté via plusieurs threads. Comparez enfin le temps d’exécution total. L'environnement de test est un Mac pro dual-core. Remarque : Afin de réduire l'impact de la perte de performances de la bibliothèque de threads elle-même sur les résultats des tests, le code monothread utilise également ici des threads. Exécutez-le simplement deux fois de manière séquentielle pour simuler un seul thread.
Un seul thread exécuté séquentiellement (single_thread.py)#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter(): i = 0 for _ in range(100000000): i = i + 1 return Truedef main(): thread_array = {} start_time = time.time() for tid in range(2): t = Thread(target=my_counter) t.start() t.join() end_time = time.time() print("Total time: {}".format(end_time - start_time))if name == 'main': main()Deux threads simultanés exécutés simultanément (multi_thread.py)#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter(): i = 0 for _ in range(100000000): i = i + 1 return Truedef main(): thread_array = {} start_time = time.time() for tid in range(2): t = Thread(target=my_counter) t.start() thread_array[tid] = t for i in range(2): thread_array[i].join() end_time = time.time() print("Total time: {}".format(end_time - start_time))if name == 'main': main()Le l'image ci-dessous est le résultat du test Vous pouvez voir que python est en fait 45 % plus lent qu'un seul thread en mode multi-thread. Selon l'analyse précédente, même avec l'existence du verrou global GIL, le multithreading sérialisé devrait avoir la même efficacité que le monothreading. Alors comment a-t-il pu y avoir un si mauvais résultat ? Analysons les raisons de cela à travers le principe de mise en œuvre du GIL. Défauts de la conception actuelle de GILMéthode de planification basée sur le nombre de pcodesSelon les idées de la communauté Python, la planification des threads du système d’exploitation lui-même est déjà très mature. Une fois stable, il n’est pas nécessaire de créer le vôtre. Ainsi, un thread Python est un pthread dulangage C et est planifié via l'algorithme de planification du système d'exploitation (par exemple, linux est CFS). Afin de permettre à chaque thread d'utiliser le temps CPU de manière égale, Python calculera le nombre de microcodes actuellement exécutés et forcera la libération du GIL lorsqu'il atteint un certain seuil. À ce moment-là, la planification des threads du système d'exploitation sera également déclenchée (bien entendu, le fait que le changement de contexte soit réellement effectué est déterminé par le système d'exploitation).
Pseudocodewhile True: acquire GIL for i in 1000: do something release GIL /* Give Operating System a chance to do thread scheduling */Ce mode ne pose aucun problème lorsqu'il n'y a qu'un seul cœur CPU. N'importe quel thread peut obtenir avec succès le GIL lorsqu'il est réveillé (car la planification des threads n'aura lieu que lorsque le GIL sera libéré). Mais lorsque le processeur possède plusieurs cœurs, des problèmes surviennent. Comme vous pouvez le voir sur le pseudocode, il n'y a presque aucun écart entreet
PS : Bien sûr, cette implémentation est primitive et moche. L'interaction entre GIL et la planification des threads est progressivement améliorée dans chaque version de Python. Par exemple, essayez d'abord de maintenir le GIL tout en effectuant un changement de contexte de thread, relâchez le GIL en attendant les E/S, etc. Mais ce qui ne peut pas être changé, c'est que l'existence de GIL rend plus luxueuse l'opération déjà coûteuse de planification des threads du système d'exploitation. Lecture approfondie sur l'impact de GILAfin de comprendre intuitivement l'impact sur les performances de GIL sur le multi-threading, voici un tableau de résultats de test directement emprunté (voir l'image ci-dessous). La figure montre l'exécution de deux threads sur un processeur dual-core. Les deux threads sont des threads informatiques gourmands en CPU. La partie verte indique que le thread est en cours d'exécution et effectue des calculs utiles. La partie rouge indique l'heure à laquelle le thread devait se réveiller, mais n'a pas pu obtenir le GIL et n'a pas pu effectuer des calculs efficaces.release GIL
. Ainsi, lorsque d'autres threads sur d'autres cœurs sont réveillés, dans la plupart des cas, le thread principal a de nouveau acquis le GIL. À ce stade, le thread qui est réveillé pour l'exécution ne peut que perdre du temps CPU en vain, en regardant un autre thread s'exécuter joyeusement avec GIL. Puis, après avoir atteint l'heure de commutation, il entre en état d'attente, est à nouveau réveillé et attend à nouveau, répétant ainsi le cercle vicieux.acquire GIL
Comme le montre la figure, l'existence de GIL empêche le multithreading d'utiliser pleinement les capacités de traitement simultané des processeurs multicœurs.
Les threads Python gourmands en E/S peuvent-ils bénéficier du multithreading ? Jetons un coup d'œil aux résultats des tests ci-dessous. La signification des couleurs est la même que sur la photo ci-dessus. La partie blanche indique que le thread IO est en attente. On peut voir que lorsque le thread IO reçoit le paquet de données et provoque la commutation du terminal, il est toujours incapable d'obtenir le verrou GIL en raison de l'existence d'un thread gourmand en CPU, ce qui entraîne une boucle d'attente sans fin. Un résumé simple est le suivant : le multithread de Python sur les processeurs multicœurs n'a un effet positif sur les calculs gourmands en E/S que lorsqu'il y a au moins un thread gourmand en CPU, l'efficacité du multithread ; Baissera considérablement en raison de GIL. Comment éviter d'être affecté par GIL Cela dit, si je ne mentionne pas la solution, ce n'est qu'un article de vulgarisation scientifique, mais c'est inutile. GIL est si mauvais, y a-t-il un moyen de contourner ce problème ? Jetons un coup d'œil aux solutions disponibles.Utiliser le multitraitement pour remplacer Thread
L'émergence de la bibliothèque multitraitement vise en grande partie à compenser l'inefficacité de la bibliothèque de threads due à GIL. Il réplique complètement un ensemble d'interfaces fournies par thread pour faciliter la migration. La seule différence est qu'il utilise plusieurs processus au lieu de plusieurs threads. Chaque processus possède son propre GIL indépendant, il n'y aura donc aucun conflit de GIL entre les processus.
Bien sûr, le multitraitement n'est pas une panacée. Son introduction augmentera la difficulté de la communication des données et de la synchronisation entre les threads temporels du programme. Prenons le compteur comme exemple. Si nous voulons que plusieurs threads accumulent la même variable , pour le thread, déclarez une variable globale et enveloppez trois lignes avec le contexte thread.Lock. En multitraitement, puisque les processus ne peuvent pas voir les données des autres, ils peuvent uniquement déclarer une file d'attente dans le thread principal, la placer puis l'obtenir, ou utiliser la mémoire partagée. Ce coût supplémentaire de mise en œuvre rend le codage de programmes multithread, déjà très pénible, encore plus pénible. Quelles sont les difficultés spécifiques ? Les lecteurs intéressés peuvent lire davantage cet article
Utiliser d'autres analyseurs
Comme mentionné précédemment, puisque GIL n'est qu'un produit de CPython, les autres analyseurs sont-ils meilleurs ? Oui, les analyseurs comme JPython et IronPython ne nécessitent pas l'aide du GIL en raison de la nature de leurs langages d'implémentation. Cependant, en utilisant Java/C# pour l'implémentation de l'analyseur, ils ont également perdu l'opportunité de profiter des nombreuses fonctionnalités utiles des modules de langage C de la communauté. Ces analyseurs ont donc toujours été relativement spécialisés. Après tout, tout le monde choisira le premier plutôt que la fonction au début,
Done is better than perfect
.Alors c’est désespéré ?
Bien sûr, la communauté Python travaille également très dur pour améliorer continuellement le GIL, et essaie même de supprimer le GIL. Et il y a eu de nombreuses améliorations dans chaque version mineure. Les lecteurs intéressés peuvent lire davantage cette diapositive. Une autre amélioration Retravailler le GIL - Changer la granularité de commutation du comptage des opcodes au comptage des tranches de temps - Éviter que le thread qui a récemment libéré le verrou GIL ne soit à nouveau planifié immédiatement - NouveauThreadPrioritéFonction (les threads hautement prioritaires peuvent forcer d'autres threads à libérer les verrous GIL qu'ils détiennent)
Résumé
Python GIL est en fait une combinaison de fonctionnalités et de performances. C'est le produit Il s'agit d'un compromis entre le temps et l'espace, en particulier la rationalité de son existence, et il comporte également des facteurs objectifs difficiles à modifier. De l'analyse de cette partie, nous pouvons tirer les conclusions simples suivantes : - En raison de l'existence de GIL, seuls plusieurs threads dans le scénario IO Bound obtiendront de meilleures performances - Si vous souhaitez programmer avec des performances de calcul parallèle élevées, vous pouvez envisager en utilisant le noyau Certaines parties sont également transformées en modules C, ou simplement implémentées dans d'autres langages - GIL continuera d'exister pendant longtemps, mais il continuera d'être amélioré
Référence
Le problème le plus difficile de Python Documents officiels sur GIL Revisiter les priorités des threads et le nouveau GIL
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!