Fabrication industrielle
Internet des objets industriel | Matériaux industriels | Entretien et réparation d'équipement | Programmation industrielle |
home  MfgRobots >> Fabrication industrielle >  >> Industrial programming >> Python

Multithreading en Python avec exemple :Apprendre GIL en Python

Le langage de programmation Python vous permet d'utiliser le multitraitement ou le multithreading. Dans ce didacticiel, vous apprendrez à écrire des applications multithreads en Python.

Qu'est-ce qu'un fil ?

Un thread est une unité d'exécution en programmation concurrente. Le multithreading est une technique qui permet à un processeur d'exécuter plusieurs tâches d'un 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 ?

Multithreading en Python La programmation 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.

Dans ce tutoriel, vous apprendrez,

Qu'est-ce que le multitraitement ?

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

Multithreading Python contre multitraitement

Pour comprendre les processus et les threads, considérez 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 processeur l'exécute. L'instance du programme en cours d'exécution est appelée le processus.

Chaque processus aura 2 composants fondamentaux :

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 afin de faciliter le parallélisme.

Pourquoi utiliser le multithread ?

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.

Multi-threading Python

Python prend en charge les constructions pour le multitraitement ainsi que le multithreading. Dans ce didacticiel, vous vous concentrerez principalement sur la mise en œuvre du multithread applications avec python. Il existe deux modules principaux qui peuvent être utilisés pour gérer les threads en Python :

  1. Le fil et
  2. Le threading modules

Cependant, en python, il existe également ce qu'on appelle un verrou d'interpréteur global (GIL). Cela ne permet pas beaucoup de gain de performances et peut même réduire les performances de certaines applications multithread. Vous apprendrez tout à ce sujet 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 de thread et le module de thread .

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

Vous devez utiliser le threading de niveau supérieur module pour les applications que vous avez l'intention de déployer. Le module de fil de discussion n'a été couvert 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, maintenant vous avez 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 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 les modules time et 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é par le start_new_thread méthode. 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é son exécution.
  3. Il s'agit de la section principale de votre programme. Ici, vous appelez simplement le start_new_thread méthode avec le thread_test fonction comme argument. Cela créera un nouveau thread pour la fonction que vous passez comme argument et commencera à l'exécuter. Notez que vous pouvez remplacer ceci (thread_ test) avec 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 la norme de facto pour la gestion des applications multithread. Il fournit un large éventail de fonctionnalités par rapport au module de thread.

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

Nom de la fonction Description
activeCount() Renvoie le nombre de Thread objets encore vivants
fil courant() Renvoie l'objet courant de la classe Thread.
énumérer() Répertorie tous les objets Thread actifs.
estDaemon() Renvoie true si le thread est un démon.
isAlive() Renvoie vrai si le fil est toujours actif.
Méthodes de classe de thread
start() Démarre l'activité d'un thread. 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.
exécuter() Cette méthode indique l'activité d'un thread et peut être remplacée par une classe qui étend la classe Thread.
join() Il bloque l'exécution d'autre code 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 multithreads à 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.

La façon la plus courante 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 séparé de contrôle.

Ainsi, lors de l'écriture d'une application multithread, vous ferez ce qui suit :

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

Une fois qu'un objet thread a été créé, le 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'à la fin de l'activité en cours.

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

  1. Cette partie est identique à notre exemple précédent. Ici, vous importez les modules time et thread qui sont utilisés pour gérer l'exécution et les retards des threads Python.
  2. Dans ce bit, vous créez une classe appelée threadtester, qui hérite ou étend le Thread classe du module de threading. 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 le run() méthode dans votre application. Comme vous pouvez le voir dans l'exemple de code ci-dessus, le __init__ méthode (constructeur) a été remplacée. De même, vous avez également remplacé le run() méthode. 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 en tant qu'argument, le diminue de 1 à chaque itération et parcourt le reste du code jusqu'à ce que i devienne 0. À chaque itération, il imprime le nom du thread en cours d'exécution et dort pendant des secondes d'attente (ce qui est également pris comme argument ).
  4. 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 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'un autre code et attend que le thread sur lequel elle a été appelée se termine.

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 simultanément aux données, 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.

Interblocages et conditions de course

Avant d'en savoir plus 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 critiqueIl s'agit d'un fragment de code qui accède ou modifie des variables partagées et doit être exécuté comme une transaction atomique.
  • Changement de contexteIl 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 à partir du même point ultérieurement.

Interblocages

Les blocages sont le problème le plus redouté auquel les développeurs sont confrontés lors de l'écriture d'applications concurrentes/multithreadées en python. La meilleure façon de comprendre les impasses est d'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 spaghetti (un type de pâtes) et cinq fourchettes, comme indiqué sur le schéma.

À 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 ramassent leur fourche droite 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 (forks) en même temps. En conséquence, aucun des processus n'a la chance 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 opérations ou plus simultanément. Par exemple, considérez cette simple boucle for :

i=0; # a global variable
for 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 son 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-temps 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 à surmonter ce problème en synchronisant les threads.

Synchroniser les fils

Pour gérer les conditions de concurrence, les blocages et autres problèmes liés aux threads, le module de threading fournit le Lock objet. L'idée est que lorsqu'un thread veut accéder à une ressource spécifique, il acquiert un verrou pour cette ressource. Une fois qu'un thread a verrouillé une ressource particulière, aucun autre thread ne peut y accéder tant que le verrou n'est pas relâché. 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 __thread module. A 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éthodeacquérir () changera l'état en verrouillé et reviendra. Cependant, si l'état est verrouillé, l'appel à buyer() est bloqué jusqu'à ce que la méthode release() soit appelée par un autre thread.
  2. release() 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 DU CODE

  1. 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.
  2. Dans la première instruction, vous acquérez le verrou en appelant la méthodeacquérir(). Lorsque le verrou a été accordé, vous imprimez « verrou acquis » à la console. Une fois que tout le code que vous voulez 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 savez-vous que le verrou 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. Obstacles

Global Interpreter Lock (et comment le gérer)

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

  1. Code lié au processeur :il s'agit de tout morceau de code qui sera directement exécuté par le processeur.
  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 de la gestion des processus. Il s'assure qu'un thread peut accéder à une ressource particulière à la fois et empêche également l'utilisation simultanée d'objets et de bytecodes. Cela profite aux programmes à thread unique dans une augmentation des performances. GIL en python est très simple et facile à mettre en œuvre.

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 le thread-1 démarre, il acquiert d'abord le GIL et le verrouille.
  3. 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.
  4. Maintenant, supposons que le thread-1 attend une opération d'E/S. À ce moment, il publiera le GIL et thread-2 l'acquerra.
  5. 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.

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é.

Cela convient 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 GIL était-il nécessaire ?

Le ramasse-miettes CPython utilise une technique de gestion de la mémoire efficace connue sous le nom de 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 ramassé 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 consistait à ajouter un verrou à chaque objet, ce qui aurait entraîné des blocages et une augmentation de la surcharge des appels d'acquisition() et de libération().

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

Résumé

  • Python prend en charge 2 modules pour le multithreading :
    1. __thread module :il fournit une implémentation de bas niveau pour le threading et est obsolète.
    2. module d'enfilage  :Il fournit une implémentation de haut niveau pour le multithreading et constitue la norme actuelle.
  • Pour créer un thread à l'aide du module de threading, vous devez procéder comme suit :
    1. Créer une classe qui étend le Thread classe.
    2. Remplacer son constructeur (__init__).
    3. Remplacer son run() méthode.
    4. Créer un objet de cette classe.
  • Un thread peut être exécuté en appelant start() méthode.
  • La 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 son exécution.
  • Une condition de concurrence 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. Verrous
    2. Rlocks
    3. Sémaphores
    4. Conditions
    5. Événements, et
    6. Obstacles
  • 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 locked. S'il est appelé sur un objet verrouillé, il 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 verrouillage global de l'interpréteur est un mécanisme par lequel un seul processus d'interpréteur 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 de lourdes opérations liées au processeur, vous devez utiliser le module de multitraitement.

Python

  1. Fonction free() dans la bibliothèque C :comment l'utiliser ? Apprendre avec l'exemple
  2. Python String strip() Fonction avec EXAMPLE
  3. Python String count() avec des EXEMPLES
  4. Fonction Python round() avec EXEMPLES
  5. Fonction Python map() avec EXEMPLES
  6. Python Timeit() avec des exemples
  7. Compteur Python dans les collections avec exemple
  8. Python List count() avec des EXEMPLES
  9. Index de liste Python () avec exemple