Multithreading dans Python avec exemple : Apprenez GIL dans Python
⚡ Résumé intelligent
Multithreading dans Python Ce processus exécute plusieurs threads au sein d'un même processus, leur permettant de partager la mémoire et de fonctionner simultanément. Le module de gestion des threads crée et gère ces threads, tandis que le verrou global de l'interpréteur limite le parallélisme absolu, rendant cette technique particulièrement adaptée aux tâches nécessitant des opérations d'entrée/sortie.
Le Python Ce langage de programmation vous permet d'utiliser le multiprocessus ou le multithreading. Dans ce tutoriel, vous apprendrez à écrire des applications multithreadées. Python.
Qu'est-ce qu'un fil ?
En programmation concurrente, un thread est une unité d'exécution. Le multithreading est une technique qui permet à un processeur d'exécuter simultanément plusieurs tâches d'un même processus. Ces threads peuvent s'exécuter individuellement tout en partageant les ressources du processus.
Qu'est-ce qu'un processus ?
Un processus est fondamentalement un programme en cours d'exécution. Lorsque vous lancez une application sur votre ordinateur (comme un navigateur ou un éditeur de texte), le système d'exploitation crée un processus. processus.
Qu’est-ce que le multithreading Python?
Multithreading dans Python La programmation multithread est une technique bien connue qui permet à plusieurs threads d'un processus de partager leur espace de données avec le thread principal, facilitant ainsi le partage d'informations et la communication entre les threads. Les threads sont plus légers que les processus. Plusieurs threads peuvent s'exécuter individuellement tout en partageant les ressources du processus. Le multithreading a pour but d'exécuter plusieurs tâches et fonctions simultanément.
Qu’est-ce que le multitraitement ?
Multitraitement vous permet d’exécuter simultanément plusieurs processus non liés. Ces processus ne partagent pas leurs ressources et ne communiquent pas via IPC.
Python Multithreading vs multitraitement
Pour comprendre les processus et les threads, prenons l'exemple suivant : un fichier .exe sur votre ordinateur est un programme. Lorsque vous l'ouvrez, le système d'exploitation le charge en mémoire et le processeur l'exécute. L'instance du programme en cours d'exécution est appelée processus.
Chaque processus comporte deux composantes fondamentales :
- Le Code
- Les données
Désormais, un processus peut contenir une ou plusieurs sous-parties appelées threads. Cela dépend de l'architecture du système d'exploitation. On peut considérer un thread comme une section du processus pouvant être exécutée séparément par le système d'exploitation.
Autrement dit, il s'agit d'un flux d'instructions pouvant être exécuté indépendamment par le système d'exploitation. Les threads d'un même processus partagent les données de ce processus et sont conçus pour fonctionner ensemble afin de faciliter le parallélisme.
Pourquoi utiliser le multithreading ?
Le multithreading vous permet de décomposer une application en plusieurs sous-tâches et d'exécuter ces tâches simultanément. Si vous utilisez correctement le multithreading, la vitesse, les performances et le rendu de votre application peuvent tous être améliorés.
Python multithreading
Python Il prend en charge les constructions pour le multiprocessus et le multithreading. Ce tutoriel est principalement consacré à leur implémentation. multithread demandes avec PythonIl existe deux modules principaux permettant de gérer les threads. Python:
- Le fil module, et
- Le filetage module
Cependant, dans PythonIl existe également un mécanisme appelé verrou global de l'interpréteur (GIL). Il n'apporte que peu de gains de performance et peut même… réduire les performances de certaines applications multithread. Vous apprendrez tout cela dans les prochaines sections de ce didacticiel.
Les modules Thread et Threading
Les deux modules que vous découvrirez dans ce tutoriel sont les module de fil et la module de filetage.
Cependant, le module thread est obsolète depuis longtemps. En commençant par Python 3, il a été désigné comme obsolète et n'est accessible que sous forme de _fil pour la compatibilité descendante.
Vous devriez utiliser le niveau supérieur filetage Module destiné aux applications que vous prévoyez de déployer. Le module de gestion des threads n'est abordé ici qu'à des fins pédagogiques.
Le module de fil de discussion
La syntaxe pour créer un nouveau thread à l'aide de ce module est la suivante :
thread.start_new_thread(function_name, arguments)
Très bien, vous avez maintenant couvert la théorie de base pour commencer à coder. Alors, ouvrez votre IDLE ou un bloc-notes et tapez ce qui suit :
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Enregistrez le fichier et appuyez sur F5 pour exécuter le programme. Si tout a été fait correctement, voici le résultat que vous devriez voir :
Vous en apprendrez davantage sur les conditions de course et sur la manière de les gérer dans les sections suivantes.
EXPLICATION DES CODES
- Ces instructions importent les modules `time` et `thread`, utilisés pour gérer l'exécution et le délai des opérations. Python threads.
- Ici, vous avez défini une fonction appelée fil_test, qui sera appelé par le start_new_thread Cette fonction exécute une boucle while pendant quatre itérations et affiche le nom du thread appelant. Une fois l'itération terminée, elle affiche un message indiquant que l'exécution du thread est terminée.
- Il s'agit de la section principale de votre programme. Ici, vous appelez simplement le start_new_thread méthode avec le fil_test Passez la fonction en argument. Cela créera un nouveau thread pour la fonction passée en argument et lancera son exécution. Notez que vous pouvez remplacer `thread_test` par n'importe quelle autre fonction que vous souhaitez exécuter dans un thread.
Le module de filetage
Ce module est l'implémentation de haut niveau du multithreading. Python et la norme de facto pour la gestion des applications multithread. Elle offre un large éventail de fonctionnalités par rapport au module thread.
Structure du module Threading
Voici une liste de quelques fonctions utiles définies dans ce module :
| Nom de la fonction | Description |
|---|---|
| actifCount() | Renvoie le nombre de Fil à coudre des objets qui sont encore vivants. |
| fil en cours() | Renvoie l'objet actuel de la classe Thread. |
| énumérer() | Répertorie tous les objets Thread actifs. |
| estDémon() | Renvoie vrai si le thread est un démon. |
| est vivant() | Renvoie vrai si le thread est toujours actif. |
| Méthodes de classe de thread | |
| démarrer() | Démarre l'activité d'un fil. Il ne doit être appelé qu'une seule fois pour chaque thread car il générera une erreur d'exécution s'il est appelé plusieurs fois. |
| Cours() | Cette méthode dénote l'activité d'un thread et peut être remplacée par une classe qui étend la classe Thread. |
| joindre() | Il bloque l'exécution d'autres codes jusqu'à ce que le thread sur lequel la méthode join() a été appelée soit terminé. |
Histoire : la classe Thread
Avant de commencer à coder des programmes multithreadés à l'aide du module threading, il est crucial de comprendre la classe Thread. La classe Thread est la classe principale qui définit le modèle et les opérations d'un thread. Python.
La méthode la plus courante pour créer un multithread Python L'objectif de cette application est de déclarer une classe qui étend la classe Thread et redéfinit sa méthode run().
La classe Thread, en résumé, signifie une séquence de code qui s'exécute dans un environnement distinct. fil de contrôle.
Ainsi, lors de l’écriture d’une application multithread, vous effectuerez les opérations suivantes :
- définir une classe qui étend la classe Thread
- Remplacer le __init__ constructeur
- Remplacer le Cours() méthode
Une fois qu'un objet thread a été créé, le démarrer() Cette méthode peut être utilisée pour démarrer l'exécution de cette activité, et la joindre() La méthode peut être utilisée pour bloquer tous les autres codes jusqu'à la fin de l'activité en cours.
Essayons maintenant d'utiliser le module de gestion des threads pour implémenter votre exemple précédent. Lancez à nouveau votre IDLE et saisissez ce qui suit :
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
Ce sera le résultat lorsque vous exécuterez le code ci-dessus :
EXPLICATION DES CODES
- Cette partie est identique à notre exemple précédent. Ici, vous importez les modules `time` et `thread`, qui servent à gérer l'exécution et les délais. Python threads.
- Dans ce bit, vous créez une classe appelée threadtester, qui hérite ou étend le Fil à coudre classe du module de threading. C'est l'une des méthodes les plus courantes pour créer des threads. PythonToutefois, vous ne devez surcharger que le constructeur et le Cours() méthode dans votre application. Comme vous pouvez le voir dans l'exemple de code ci-dessus, le __init__ La méthode (constructeur) a été remplacée. De même, vous avez également remplacé le Cours() méthode. Il contient le code que vous souhaitez exécuter dans un thread. Dans cet exemple, vous avez appelé la fonction thread_test().
- Il s'agit de la méthode thread_test(), qui prend la valeur de i En tant qu'argument, il le décrémente de 1 à chaque itération et parcourt le reste du code jusqu'à ce que i devienne 0. À chaque itération, il affiche le nom du thread en cours d'exécution et attend pendant wait secondes (qui est également pris en argument).
- thread1 = threadtester(1, « First Thread », 1) Ici, nous créons un thread et transmettons les trois paramètres que nous avons déclarés dans __init__. Le premier paramètre est l'identifiant du thread, le deuxième paramètre est le nom du thread et le troisième paramètre est le compteur, qui détermine combien de fois la boucle while doit être exécutée.
- thread2.start() La méthode start() permet de démarrer l'exécution d'un thread. En interne, la fonction start() appelle la méthode run() de votre classe.
- thread3.join() La méthode join() bloque l'exécution d'autres codes et attend la fin du thread sur lequel elle a été appelée.
Comme vous le savez déjà, les threads appartenant à un même processus ont accès à la mémoire et aux données de ce processus. Par conséquent, si plusieurs threads tentent de modifier ou d'accéder simultanément aux données, des erreurs peuvent survenir.
Dans la section suivante, vous découvrirez les différents types de complications qui peuvent survenir lorsque des threads accèdent aux données, ainsi que la section critique sans vérification des transactions d'accès existantes.
Impasses et conditions de course
Avant d'aborder les notions d'interblocage et de conditions de concurrence, il sera utile de comprendre quelques définitions de base liées à la programmation concurrente :
- Section critique: Il s'agit d'un fragment de code qui accède à des variables partagées ou les modifie et qui doit être exécuté dans le cadre d'une transaction atomique.
- Changement de contexte : Il s'agit du processus suivi par un processeur pour stocker l'état d'un thread avant de passer d'une tâche à une autre, afin qu'il puisse être repris ultérieurement au même point.
Les impasses
Les impasses sont le problème le plus redouté auquel les développeurs sont confrontés lors de l'écriture d'applications concurrentes/multithreadées. PythonLa meilleure façon de comprendre les interblocages est d'utiliser le problème classique de l'informatique connu sous le nom de blocage. La Cuisine PhiloProblème des Sophers.
L’énoncé du problème pour les philosophes de la restauration est le suivant :
Cinq philosophes sont assis autour d'une table ronde avec cinq assiettes de spaghettis (un type de pâtes) et cinq fourchettes, comme le montre le schéma.
La Cuisine PhiloProblème des Sophers
À un moment donné, un philosophe doit soit manger, soit réfléchir.
De plus, un philosophe doit prendre les deux fourchettes adjacentes à lui (c'est-à-dire les fourchettes gauche et droite) avant de pouvoir manger les spaghettis. Le problème de l’impasse survient lorsque les cinq philosophes prennent simultanément leur bonne fourchette.
Puisque chacun des philosophes possède une fourchette, ils attendront tous que les autres posent leur fourchette. Résultat : aucun d’entre eux ne pourra manger des spaghettis.
De même, dans un système concurrent, un blocage se produit lorsque différents threads ou processus (philosophes) tentent d'acquérir les ressources système partagées (forks) en même temps. En conséquence, aucun des processus n’a la possibilité de s’exécuter car ils attendent une autre ressource détenue par un autre processus.
Conditions de course
Une condition de concurrence est un état indésirable d'un programme qui survient lorsqu'un système exécute simultanément deux opérations ou plus. Prenons par exemple cette simple boucle for :
i=0; # a global variable for x in range(100): print(i) i+=1;
Si vous créez n Étant donné le nombre de threads exécutant ce code simultanément, il est impossible de déterminer la valeur de `i` (partagée par les threads) à la fin de l'exécution du programme. En effet, dans un environnement multithread réel, les threads peuvent se chevaucher et la valeur de `i` récupérée et modifiée par un thread peut changer entre deux accès par d'autres threads.
Ce sont les deux principales catégories de problèmes pouvant survenir dans un environnement multithread ou distribué. Python application. Dans la section suivante, vous apprendrez comment résoudre ce problème en synchronisant les threads.
Syncfils de discussion
Pour gérer les conditions de concurrence, les blocages et autres problèmes liés aux threads, le module de threading fournit le Verrouillage objet. L'idée est que lorsqu'un thread souhaite accéder à une ressource spécifique, il acquiert un verrou pour cette ressource. Une fois qu'un thread verrouille une ressource particulière, aucun autre thread ne peut y accéder jusqu'à ce que le verrou soit libéré. En conséquence, les modifications apportées à la ressource seront atomiques et les conditions de concurrence seront évitées.
Un verrou est une primitive de synchronisation de bas niveau implémentée par le _fil module. À tout moment, un verrou peut se trouver dans l'un des deux états suivants : fermé or déverrouillé. Il prend en charge deux méthodes :
- acquérir(): Lorsque l'état de verrouillage est déverrouillé, l'appel à la méthode `acquire()` modifie l'état en verrouillé et retourne une valeur. En revanche, si l'état est verrouillé, l'appel à `acquire()` est bloqué jusqu'à ce que la méthode `release()` soit appelée par un autre thread.
- libérer(): La méthode release() est utilisée pour définir l'état sur déverrouillé, c'est-à-dire pour libérer un verrou. Il peut être appelé par n'importe quel thread, pas nécessairement celui qui a acquis le verrou.
Voici un exemple d'utilisation de verrous dans vos applications. Lancez votre IDLE et tapez ce qui suit :
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Maintenant, appuyez sur F5. Vous devriez voir une sortie comme celle-ci :
EXPLICATION DES CODES
- Ici, vous créez simplement un nouveau verrou en appelant le threading.Lock() fonction d'usine. En interne, Lock() renvoie une instance de la classe Lock concrète la plus efficace maintenue par la plate-forme.
- Dans la première instruction, vous obtenez le verrou en appelant la méthode acquire(). Lorsque le verrouillage a été accordé, vous imprimez « serrure acquise » à la console. Une fois que tout le code que vous souhaitez que le thread exécute est terminé, vous libérez le verrou en appelant la méthode release().
La théorie est bonne, mais comment savoir si le verrou a réellement fonctionné ? En observant le résultat, on constate que chaque instruction d'affichage n'affiche qu'une seule ligne à la fois. Rappelons que, dans un exemple précédent, l'affichage était aléatoire car plusieurs threads accédaient simultanément à la méthode `print()`. Ici, la fonction `print` n'est appelée qu'après l'acquisition du verrou. Les résultats s'affichent donc ligne par ligne.
Hormis les serrures, Python Il prend également en charge d'autres mécanismes de gestion de la synchronisation des threads, comme indiqué ci-dessous :
- RLocks
- Semaphores
- Conditions
- événements, et
- Barrières
Verrouillage global de l'interprète (et comment y faire face)
Avant d'entrer dans les détails de PythonDans le cadre du GIL, définissons quelques termes qui seront utiles pour comprendre la section suivante :
- Code lié au processeur : cela désigne tout morceau de code qui sera directement exécuté par le processeur.
- Code lié aux E/S : il peut s’agir de tout code accédant au système de fichiers via le système d’exploitation.
- CPython: c'est la référence la mise en oeuvre of Python et peut être décrit comme l'interpréteur écrit en C et Python (langage de programmation).
Qu'est-ce que GIL dans Python?
Verrouillage d'interprète global (GIL) in Python Un verrou de processus (ou mutex) est utilisé lors de la gestion des processus. Il garantit qu'un seul thread peut accéder à une ressource spécifique à la fois et empêche l'utilisation simultanée d'objets et de bytecode. Ceci améliore les performances des programmes monothread. Python est très simple et facile à mettre en œuvre.
Un verrou peut être utilisé pour garantir qu'un seul thread a accès à une ressource particulière à un moment donné.
L'une des caractéristiques de Python c'est qu'il utilise un verrou global sur chaque processus d'interprétation, ce qui signifie que chaque processus traite le Python l'interpréteur lui-même en tant que ressource.
Par exemple, supposons que vous ayez écrit un Python Ce programme utilise deux threads pour effectuer à la fois des opérations sur le processeur et des opérations d'entrée/sortie. Voici ce qui se passe lors de son exécution :
- Le Python L'interpréteur crée un nouveau processus et lance les threads.
- Lorsque le thread-1 démarre, il acquiert d'abord le GIL et le verrouille.
- Si le thread-2 veut s'exécuter maintenant, il devra attendre que le GIL soit libéré même si un autre processeur est libre.
- Supposons maintenant que le thread 1 attend une opération d’E/S. À ce moment-là, il libérera le GIL et le thread-2 l'acquérira.
- Après avoir terminé les opérations d'E/S, si le thread-1 veut s'exécuter maintenant, il devra à nouveau attendre que le GIL soit libéré par le thread-2.
De ce fait, un seul thread peut accéder à l'interpréteur à la fois, ce qui signifie qu'un seul thread sera en cours d'exécution. Python code à un moment donné.
Cela ne pose pas de problème sur un processeur monocœur, car celui-ci utilise le partage de temps (voir la première section de ce tutoriel) pour gérer les threads. Cependant, sur un processeur multicœur, une fonction gourmande en ressources CPU exécutée sur plusieurs threads aura un impact considérable sur l'efficacité du programme, car elle n'utilisera pas simultanément tous les cœurs disponibles.
Pourquoi GIL était-il nécessaire ?
Les CPython Le ramasse-miettes utilise une technique efficace de gestion de la mémoire appelée comptage de références. Voici comment cela fonctionne : chaque objet dans Python Chaque objet possède un compteur de références, incrémenté lors de son assignation à une nouvelle variable ou de son ajout à un conteneur (tuple, liste, etc.). Ce compteur est décrémenté lorsque l'objet sort de sa portée ou lors de l'appel à la méthode `del`. Lorsque le compteur de références d'un objet atteint zéro, celui-ci est collecté par le ramasse-miettes et la mémoire allouée est libérée.
Mais le problème est que la variable de comptage de références est sujette aux conditions de concurrence, comme toute autre variable globale. Pour résoudre ce problème, les développeurs de Python Nous avons opté pour le verrou global de l'interpréteur. L'autre solution consistait à ajouter un verrou à chaque objet, ce qui aurait entraîné des blocages et une surcharge accrue due aux appels à `acquire()` et `release()`.
Par conséquent, GIL constitue une restriction importante pour le multithreading Python Les programmes exécutant des opérations gourmandes en ressources CPU (les rendant de fait monothread). Si vous souhaitez utiliser plusieurs cœurs de processeur dans votre application, utilisez le multitraitement module à la place.









