Exemple de multithreading en Python avec Global Interpreter Lock (GIL)

Table des matières:

Anonim

Le langage de programmation python vous permet d'utiliser le multiprocessing ou le multithreading.Dans ce tutoriel, vous apprendrez à écrire des applications multithread en Python.

Qu'est-ce qu'un fil?

Un thread est une unité d'exécution sur la programmation simultanée. Le multithreading est une technique qui permet à un processeur d'exécuter plusieurs tâches d'un même processus en même temps. Ces threads peuvent s'exécuter individuellement tout en partageant leurs ressources de processus.

Qu'est-ce qu'un processus?

Un processus est essentiellement le programme en cours d'exécution. Lorsque vous démarrez une application sur votre ordinateur (comme un navigateur ou un éditeur de texte), le système d'exploitation crée un processus.

Qu'est-ce que le multithreading en Python?

Le multithreading dans la programmation Python est une technique bien connue dans laquelle plusieurs threads d'un processus partagent leur espace de données avec le thread principal, ce qui rend le partage d'informations et la communication au sein des threads faciles et efficaces. Les threads sont plus légers que les processus. Plusieurs threads peuvent s'exécuter individuellement tout en partageant leurs ressources de processus. Le but du multithreading est d'exécuter plusieurs tâches et cellules de fonction en même temps.

Qu'est-ce que le multitraitement?

Le multitraitement vous permet d'exécuter simultanément plusieurs processus indépendants. Ces processus ne partagent pas leurs ressources et ne communiquent pas via IPC.

Multithreading Python vs multitraitement

Pour comprendre les processus et les threads, envisagez ce scénario: 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 CPU l'exécute. L'instance du programme en cours d'exécution est appelée le processus.

Chaque processus aura 2 composants fondamentaux:

  • 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. Vous pouvez considérer un thread comme une section du processus qui peut être exécutée séparément par le système d'exploitation.

En d'autres termes, il s'agit d'un flux d'instructions qui peut ê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 pour faciliter le parallélisme.

Dans ce tutoriel, vous apprendrez,

  • Qu'est-ce qu'un fil?
  • Qu'est-ce qu'un processus?
  • Qu'est-ce que le multithreading?
  • Qu'est-ce que le multitraitement?
  • Multithreading Python vs multitraitement
  • Pourquoi utiliser le multithreading?
  • MultiThreading Python
  • Les modules Thread et Threading
  • Le module de fil
  • Le module de filetage
  • Deadlocks et conditions de course
  • Synchroniser les threads
  • Qu'est-ce que GIL?
  • Pourquoi avait-on besoin de GIL?

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.

MultiThreading Python

Python prend en charge les constructions tant pour le multitraitement que pour le multithreading. Dans ce didacticiel, vous vous concentrerez principalement sur l'implémentation d' applications multithread avec python. Il existe deux modules principaux qui peuvent être utilisés pour gérer les threads en Python:

  1. Le module de thread , et
  2. Le module de filetage

Cependant, en python, il existe également ce qu'on appelle un verrou d'interpréteur global (GIL). Cela ne permet pas de gagner beaucoup de performances et peut même réduire les performances de certaines applications multithread. Vous en saurez plus dans les prochaines sections de ce didacticiel.

Les modules Thread et Threading

Les deux modules que vous découvrirez dans ce didacticiel sont le module thread et le module threading .

Cependant, le module de thread est obsolète depuis longtemps. À partir de Python 3, il a été désigné comme obsolète et n'est accessible qu'en tant que __thread pour des raisons de compatibilité descendante.

Vous devez utiliser le module de thread de niveau supérieur pour les applications que vous envisagez de déployer. Le module de discussion n'a été traité ici qu'à des fins éducatives.

Le module de fil

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 timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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 la sortie que vous devriez voir:

Vous en apprendrez plus sur les conditions de course et comment les gérer dans les sections à venir

EXPLICATION DU CODE

  1. Ces instructions importent le module de temps et de thread qui sont utilisés pour gérer l'exécution et le retard des threads Python.
  2. Ici, vous avez défini une fonction appelée thread_test, qui sera appelée par la méthode start_new_thread . La fonction exécute une boucle while pendant quatre itérations et imprime le nom du thread qui l'a appelée. Une fois l'itération terminée, il imprime un message indiquant que le thread a terminé l'exécution.
  3. Ceci est la section principale de votre programme. Ici, vous appelez simplement la méthode start_new_thread avec la fonction thread_test comme argument.

    Cela créera un nouveau thread pour la fonction que vous passez en argument et commencera à l'exécuter. Notez que vous pouvez remplacer ceci (thread _ test) par toute autre fonction que vous souhaitez exécuter en tant que thread.

Le module de filetage

Ce module est l'implémentation de haut niveau du threading en python et le standard de facto pour la gestion des applications multithread. Il offre un large éventail de fonctionnalités par rapport au module de filetage.

Structure du module de filetage

Voici une liste de quelques fonctions utiles définies dans ce module:

Nom de la fonction Description
activeCount () Renvoie le nombre d' objets Thread qui sont toujours en vie
currentThread () Renvoie l'objet actuel de la classe Thread.
énumérer() Répertorie tous les objets Thread actifs.
isDaemon () Renvoie true si le thread est un démon.
est vivant() Renvoie true si le thread est toujours actif.
Méthodes de classe de thread
début() Démarre l'activité d'un fil. Il ne doit être appelé qu'une seule fois pour chaque thread car il lèvera 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.
rejoindre() Il bloque l'exécution d'un autre code jusqu'à ce que le thread sur lequel la méthode join () a été appelée se termine.

Backstory: La classe de fil

Avant de commencer à coder des programmes multithread à l'aide du module de 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 en python.

Le moyen le plus courant de créer une application python multithread est de déclarer une classe qui étend la classe Thread et remplace sa méthode run ().

La classe Thread, en résumé, signifie une séquence de code qui s'exécute dans un thread de contrôle distinct .

Ainsi, lors de l'écriture d'une application multithread, vous effectuerez les opérations suivantes:

  1. définir une classe qui étend la classe Thread
  2. Remplacer le constructeur __init__
  3. Remplacer la méthode run ()

Une fois qu'un objet thread a été créé, la méthode start () peut être utilisée pour commencer l'exécution de cette activité et la méthode join () peut être utilisée pour bloquer tout autre code jusqu'à ce que l'activité en cours se termine.

Maintenant, essayons d'utiliser le module de threading pour implémenter votre exemple précédent. Encore une fois, lancez votre IDLE et tapez ce qui suit:

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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 la sortie lorsque vous exécuterez le code ci-dessus:

EXPLICATION DU CODE

  1. Cette partie est la même que notre exemple précédent. Ici, vous importez le module time et thread qui sont utilisés pour gérer l'exécution et les délais des threads Python.
  2. Dans ce bit, vous créez une classe appelée threadtester, qui hérite ou étend la classe Thread du module de thread. C'est l'un des moyens les plus courants de créer des threads en python. Cependant, vous ne devez remplacer que le constructeur et la méthode run () dans votre application. Comme vous pouvez le voir dans l'exemple de code ci-dessus, la méthode __init__ (constructeur) a été remplacée.

    De même, vous avez également remplacé la méthode run () . Il contient le code que vous souhaitez exécuter dans un thread. Dans cet exemple, vous avez appelé la fonction thread_test ().

  3. C'est la méthode thread_test () qui prend la valeur de i comme argument, la diminue de 1 à chaque itération et parcourt le reste du code jusqu'à ce que i devienne 0. A chaque itération, elle imprime le nom du thread en cours d'exécution et dort pendant des secondes d'attente (ce qui est également considéré comme un argument).
  4. thread1 = testeur de thread (1, "Premier thread", 1)

    Ici, nous créons un thread et passons les trois paramètres que nous avons déclarés dans __init__. Le premier paramètre est l'id 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 s'exécuter.

  5. thread2.start ()

    La méthode start est utilisée pour démarrer l'exécution d'un thread. En interne, la fonction start () appelle la méthode run () de votre classe.

  6. 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 qui sont dans le 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 aux données simultanément, des erreurs peuvent s'infiltrer.

Dans la section suivante, vous verrez les différents types de complications qui peuvent apparaître lorsque les threads accèdent aux données et à la section critique sans vérifier les transactions d'accès existantes.

Deadlocks et conditions de course

Avant d'en apprendre davantage sur les blocages et les conditions de concurrence, il sera utile de comprendre quelques définitions de base liées à la programmation simultanée:

  • Section critique

    C'est un fragment de code qui accède ou modifie des variables partagées et doit être effectué comme une transaction atomique.

  • Changement de contexte

    C'est le processus suivi par un processeur pour stocker l'état d'un thread avant de passer d'une tâche à une autre afin de pouvoir le reprendre ultérieurement à partir du même point.

Les impasses

Les blocages sont le problème le plus redouté auquel les développeurs sont confrontés lors de l'écriture d'applications simultanées / multithread en python. La meilleure façon de comprendre les blocages consiste à utiliser l'exemple de problème informatique classique connu sous le nom de problème des philosophes de la restauration.

L'énoncé du problème pour les philosophes de la restauration est le suivant:

Cinq philosophes sont assis sur une table ronde avec cinq assiettes de spaghettis (un type de pâtes) et cinq fourchettes, comme le montre le diagramme.

Problème des philosophes de la restauration

À tout moment, un philosophe doit soit manger, soit penser.

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 leurs bonnes fourchettes simultanément.

Puisque chacun des philosophes a une fourchette, ils attendront tous que les autres posent leur fourchette. En conséquence, 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 (fourches) 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 se produit lorsqu'un système exécute deux ou plusieurs opérations simultanément. Par exemple, considérez cette simple boucle for:

i=0; # a global variablefor x in range(100):print(i)i+=1;

Si vous créez n nombre de threads qui exécutent ce code à la fois, vous ne pouvez pas déterminer la valeur de i (qui est partagée par les threads) lorsque le programme termine l'exécution. En effet, dans un environnement multithreading réel, les threads peuvent se chevaucher et la valeur de i qui a été récupérée et modifiée par un thread peut changer entre les deux lorsqu'un autre thread y accède.

Ce sont les deux principales classes de problèmes qui peuvent survenir dans une application Python multithread ou distribuée. Dans la section suivante, vous apprendrez comment résoudre ce problème en synchronisant les threads.

Synchroniser les threads

Pour gérer les conditions de concurrence critique, les blocages et d'autres problèmes liés aux threads, le module de threading fournit l' objet Lock . 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 tant que le verrou n'est pas 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 module __thread . À tout moment, un verrou peut être dans l'un des 2 états suivants: verrouillé ou déverrouillé. Il prend en charge deux méthodes:

  1. acquérir()

    Lorsque l'état de verrouillage est déverrouillé, l'appel de la méthode Acquérir () changera l'état en verrouillé et retournera. Cependant, si l'état est verrouillé, l'appel d'acquérir () est bloqué jusqu'à ce que la méthode release () soit appelée par un autre thread.

  2. Libération()

    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 threadinglock = 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 DU CODE

  1. Ici, vous créez simplement un nouveau verrou en appelant la fonction d'usine threading.Lock () . En interne, Lock () renvoie une instance de la classe Lock concrète la plus efficace maintenue par la plate-forme.
  2. Dans la première instruction, vous acquérez le verrou en appelant la méthode Acquérir (). Lorsque le verrou a été accordé, vous imprimez «verrou acquis» sur la console. Une fois que tout le code que vous voulez que le thread exécute a terminé son exécution, vous libérez le verrou en appelant la méthode release ().

La théorie est bonne, mais comment savez-vous que la serrure a vraiment fonctionné? Si vous regardez la sortie, vous verrez que chacune des instructions d'impression imprime exactement une ligne à la fois. Rappelez-vous que, dans un exemple précédent, les sorties de print étaient aléatoires car plusieurs threads accédaient à la méthode print () en même temps. Ici, la fonction d'impression n'est appelée qu'après l'acquisition du verrou. Ainsi, les sorties sont affichées une par une et ligne par ligne.

Outre les verrous, python prend également en charge d'autres mécanismes pour gérer la synchronisation des threads, comme indiqué ci-dessous:

  1. RLocks
  2. Sémaphores
  3. Conditions
  4. Événements, et
  5. Barrières

Global Interpreter Lock (et comment y faire face)

Avant d'entrer dans les détails du GIL de python, définissons quelques termes qui seront utiles pour comprendre la section à venir:

  1. Code lié au CPU: il s'agit de tout morceau de code qui sera directement exécuté par le CPU.
  2. Code lié aux E / S: il peut s'agir de n'importe quel code qui accède au système de fichiers via le système d'exploitation
  3. CPython: c'est l' implémentation de référence de Python et peut être décrit comme l'interpréteur écrit en C et Python (langage de programmation).

Qu'est-ce que GIL en Python?

Global Interpreter Lock (GIL) en python est un verrou de processus ou un mutex utilisé lors du traitement des processus. Il s'assure qu'un thread peut accéder à une ressource particulière à la fois et il empêche également l'utilisation d'objets et de bytecodes à la fois. Cela profite aux programmes à un seul thread dans une augmentation des performances. GIL en python est très simple et facile à implémenter.

Un verrou peut être utilisé pour s'assurer qu'un seul thread a accès à une ressource particulière à un moment donné.

L'une des caractéristiques de Python est qu'il utilise un verrou global sur chaque processus d'interpréteur, ce qui signifie que chaque processus traite l'interpréteur python lui-même comme une ressource.

Par exemple, supposons que vous ayez écrit un programme python qui utilise deux threads pour effectuer à la fois des opérations CPU et 'E / S'. Lorsque vous exécutez ce programme, voici ce qui se passe:

  1. L'interpréteur python crée un nouveau processus et génère les threads
  2. Lorsque thread-1 démarre, il acquiert d'abord le GIL et le verrouille.
  3. Si thread-2 veut s'exécuter maintenant, il devra attendre que le GIL soit libéré même si un autre processeur est libre.
  4. Maintenant, supposons que thread-1 attend une opération d'E / S. À ce moment, il publiera le GIL et thread-2 l'acquérera.
  5. Après avoir terminé les opérations d'E / S, si thread-1 veut s'exécuter maintenant, il devra à nouveau attendre que le GIL soit libéré par thread-2.

Pour cette raison, un seul thread peut accéder à l'interpréteur à tout moment, ce qui signifie qu'il n'y aura qu'un seul thread exécutant du code python à un moment donné.

C'est bien dans un processeur monocœur car il utiliserait le découpage temporel (voir la première section de ce didacticiel) pour gérer les threads. Cependant, dans le cas de processeurs multicœurs, une fonction liée au processeur s'exécutant sur plusieurs threads aura un impact considérable sur l'efficacité du programme car il n'utilisera pas tous les cœurs disponibles en même temps.

Pourquoi avait-on besoin de GIL?

Le garbage collector CPython utilise une technique de gestion de mémoire efficace appelée comptage de références. Voici comment cela fonctionne: chaque objet en python a un nombre de références, qui est augmenté lorsqu'il est affecté à un nouveau nom de variable ou ajouté à un conteneur (comme des tuples, des listes, etc.). De même, le nombre de références est diminué lorsque la référence sort de la portée ou lorsque l'instruction del est appelée. Lorsque le nombre de références d'un objet atteint 0, il est garbage collection 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 à des conditions de concurrence comme toute autre variable globale. Pour résoudre ce problème, les développeurs de python ont décidé d'utiliser le verrou d'interpréteur global. L'autre option était d'ajouter un verrou à chaque objet, ce qui aurait entraîné des blocages et une augmentation de la surcharge des appels acquises () et release ().

Par conséquent, GIL est une restriction importante pour les programmes python multithread exécutant des opérations lourdes liées au processeur (ce qui les rend effectivement mono-thread). Si vous souhaitez utiliser plusieurs cœurs de processeur dans votre application, utilisez plutôt le module multitraitement .

Résumé

  • Python prend en charge 2 modules pour le multithreading:
    1. Module __thread : il fournit une implémentation de bas niveau pour le threading et est obsolète.
    2. module de threading : il fournit une implémentation de haut niveau pour le multithreading et est le standard actuel.
  • Pour créer un thread à l'aide du module de thread, vous devez effectuer les opérations suivantes:
    1. Créez une classe qui étend la classe Thread .
    2. Remplacez son constructeur (__init__).
    3. Remplacez sa méthode run () .
    4. Créez un objet de cette classe.
  • Un thread peut être exécuté en appelant la méthode start () .
  • La méthode join () peut être utilisée pour bloquer d'autres threads jusqu'à ce que ce thread (celui sur lequel la jointure a été appelée) termine l'exécution.
  • Une condition de concurrence critique se produit lorsque plusieurs threads accèdent ou modifient une ressource partagée en même temps.
  • Cela peut être évité en synchronisant les threads.
  • Python prend en charge 6 façons de synchroniser les threads:
    1. Serrures
    2. RLocks
    3. Sémaphores
    4. Conditions
    5. Événements, et
    6. Barrières
  • Les verrous permettent uniquement à un thread particulier qui a acquis le verrou d'entrer dans la section critique.
  • Un verrou a 2 méthodes principales:
    1. Acquérir () : il définit l'état de verrouillage sur verrouillé. S'il est appelé sur un objet verrouillé, il se bloque jusqu'à ce que la ressource soit libre.
    2. release () : Il définit l'état de verrouillage sur déverrouillé et revient. S'il est appelé sur un objet déverrouillé, il renvoie false.
  • Le verrou d'interpréteur global est un mécanisme par lequel un seul processus d'interprétation CPython peut s'exécuter à la fois.
  • Il a été utilisé pour faciliter la fonctionnalité de comptage de références du ramasse-miettes de CPythons.
  • Pour créer des applications Python avec des opérations lourdes liées au processeur, vous devez utiliser le module de multitraitement.