Multithreading en Python

Le multithreading est similaire à l'exécution de plusieurs programmes différents en même temps. L'exécution de multithreads présente les avantages suivants :

  • L'utilisation de threads peut placer les tâches qui occupent longtemps un programme en arrière-plan pour traitement.
  • L'interface utilisateur peut être plus attrayante. Par exemple, lorsqu'un utilisateur clique sur un bouton pour déclencher le traitement de certains événements, une barre de progression peut apparaître pour afficher la progression du traitement.
  • La vitesse d'exécution du programme peut être accélérée.
  • Les threads sont plus utiles pour implémenter certaines tâches d'attente telles que la saisie utilisateur, la lecture et l'écriture de fichiers, et l'envoi/réception de données réseau. Dans ces cas, nous pouvons libérer certaines ressources précieuses telles que l'utilisation de la mémoire.

Les threads diffèrent encore des processus lors de l'exécution. Chaque processus indépendant possède un point d'entrée pour l'exécution du programme, une séquence d'exécution séquentielle et une sortie pour le programme. Cependant, les threads ne peuvent pas s'exécuter indépendamment ; ils doivent dépendre d'une application, qui fournit plusieurs contrôles d'exécution de threads.

Chaque thread possède son propre ensemble de registres CPU, appelé contexte du thread. Ce contexte reflète l'état des registres CPU lors de la dernière exécution du thread.

Les registres de pointeur d'instruction et de pointeur de pile sont les deux registres les plus importants du contexte de thread. Un thread s'exécute toujours dans le contexte d'un processus, et ces adresses sont utilisées pour identifier la mémoire dans l'espace d'adressage du processus qui possède le thread.

  • Les threads peuvent être préemptés (interrompus).
  • Pendant que d'autres threads s'exécutent, un thread peut être temporairement suspendu (également appelé sommeil) -- c'est le yield de thread.

Commencer à apprendre les threads Python

Il existe deux façons d'utiliser les threads en Python : avec des fonctions ou en utilisant une classe pour encapsuler des objets thread.

Approche fonctionnelle : appelez la fonction start_new_thread() du module thread pour créer un nouveau thread. La syntaxe est la suivante :

thread.start_new_thread ( function, args[, kwargs] )

Description du paramètre :

  • function - La fonction du thread.
  • args - Les arguments passés à la fonction de thread ; il doit être de type tuple.
  • kwargs - Arguments facultatifs.

Exemple (Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import thread import time # Définir une fonction pour le thread def print_time( threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print "%s: %s" % ( threadName, time.ctime(time.time()) ) # Créer deux threads try: thread.start_new_thread( print_time, ("Thread-1", 2, ) ) thread.start_new_thread( print_time, ("Thread-2", 4, ) ) except: print "Erreur : impossible de démarrer le thread" while 1: pass

La sortie de l'exécution du programme ci-dessus est la suivante :

Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009

La terminaison d'un thread repose généralement sur la fin naturelle de la fonction du thread ; vous pouvez aussi appeler thread.exit() dans la fonction du thread, ce qui déclenche une exception SystemExit afin de terminer le thread.


Module Thread

Python prend en charge les threads via deux bibliothèques standard, thread et threading. thread fournit des threads primitifs de bas niveau et un verrou simple.

Autres méthodes fournies par le module threading :

  • threading.currentThread() : Retourne la variable du thread actuel.
  • threading.enumerate() : Retourne une liste contenant les threads en cours d'exécution. En cours d'exécution signifie après le démarrage du thread et avant sa fin, sans inclure les threads avant le démarrage et après la terminaison.
  • threading.activeCount() : Retourne le nombre de threads en cours d'exécution, même résultat que len(threading.enumerate()).

En plus d'utiliser des méthodes, le module thread fournit également la classe Thread pour gérer les threads. La classe Thread fournit les méthodes suivantes :

  • run():Méthode utilisée pour représenter l'activité d'un thread.
  • start():Démarre l'activité d'un thread.
  • join([time]):Attend jusqu'à ce que le thread se termine. Cela bloque le thread appelant jusqu'à ce que la méthode join() du thread soit appelée pour terminer - sortie normale ou exception non gérée - ou qu'un délai d'attente optionnel se produise.
  • isAlive():Renvoie si le thread est actif.
  • getName():Renvoie le nom du thread.
  • setName():Définit le nom du thread.

Création de threads avec le module Threading

Pour créer un thread à l'aide du module Threading, héritez directement de threading.Thread, puis remplacez la méthode __init__ et la méthode run :

Exemple (Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import threading import time exitFlag = 0 class myThread (threading.Thread): # Hérite de la classe parent threading.Thread def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): # Écrivez le code à exécuter dans la fonction run. Le fil exécutera directement la fonction run après sa création. print "Démarrage " + self.name print_time(self.name, self.counter, 5) print "Sortie " + self.name def print_time(threadName, delay, counter): while counter: if exitFlag: (threading.Thread).exit() time.sleep(delay) print "%s: %s" % (threadName, time.ctime(time.time())) counter -= 1 # Créer un nouveau fil thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # Démarrer le fil thread1.start() thread2.start() print "Sortie du thread principal"

Le résultat de l'exécution du programme ci-dessus est le suivant :

Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2

Synchronisation des threads

Si plusieurs threads modifient ensemble une même donnée, des résultats imprévisibles peuvent se produire. Pour garantir l'exactitude des données, plusieurs threads doivent être synchronisés.

L'utilisation des objets Lock et RLock de Thread permet d'implémenter une synchronisation de threads simple. Ces deux objets possèdent tous deux des méthodes acquire et release. Pour les données qui ne nécessitent l'opération que d'un seul thread à la fois, ses opérations peuvent être placées entre les méthodes acquire et release. Comme suit :

L'avantage du multithreading est qu'il peut exécuter plusieurs tâches en même temps (du moins, c'est l'impression que cela donne). Cependant, lorsque des threads doivent partager des données, des problèmes de désynchronisation des données peuvent survenir.

Considérez la situation suivante : tous les éléments d'une liste valent 0. Le thread "set" remplace tous les éléments par 1 de l'arrière vers l'avant, tandis que le thread "print" est chargé de lire la liste de l'avant vers l'arrière et de l'afficher.

Ensuite, il est possible que, lorsque le thread "set" commence à modifier, le thread "print" vienne imprimer la liste, et que la sortie devienne moitié 0 et moitié 1. C'est une désynchronisation des données. Pour éviter cette situation, la notion de verrous est introduite.

Un verrou a deux états : verrouillé et déverrouillé. Chaque fois qu'un thread comme "set" veut accéder à des données partagées, il doit d'abord acquérir le verrou ; si un autre thread comme "print" a déjà acquis le verrou, alors faites patienter le thread "set", ce qui est un blocage synchrone ; après que le thread "print" a terminé d'accéder et libéré le verrou, laissez le thread "set" continuer.

Avec un tel traitement, lors de l'impression de la liste, soit tous les 0 sont affichés, soit tous les 1 sont affichés, et la situation embarrassante de moitié 0 et moitié 1 ne se produira plus.

Exemple (Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print "Démarrage " + self.name # Acquérir le verrou, renvoyer True après avoir acquis le verrou avec succès # Si le paramètre facultatif timeout n'est pas renseigné, il bloquera jusqu'à ce que le verrou soit acquis # Sinon, il renverra False après l'expiration du timeout threadLock.acquire() print_time(self.name, self.counter, 3) # Libérer le verrou threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print "%s: %s" % (threadName, time.ctime(time.time())) counter -= 1 threadLock = threading.Lock() threads = [] # Créer un nouveau thread thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # Démarrer un nouveau thread thread1.start() thread2.start() # Ajouter le thread à la liste des threads threads.append(thread1) threads.append(thread2) # Attendre que tous les threads se terminent for t in threads: t.join() print "Sortie du thread principal"

File de priorité des threads

Le module Queue en Python fournit des classes de file d'attente synchronisées et thread-safe, notamment la file FIFO (premier entré, premier sorti) Queue, la file LIFO (dernier entré, premier sorti) LifoQueue, et la file de priorité PriorityQueue. Ces files implémentent toutes des primitives de verrouillage et peuvent être utilisées directement en multithreading. Les files peuvent être utilisées pour réaliser une synchronisation entre threads.

Méthodes courantes du module Queue :

  • Queue.qsize() Renvoie la taille de la file d'attente
  • Queue.empty() Si la file est vide, renvoie True, sinon False
  • Queue.full() Si la file est pleine, renvoie True, sinon False
  • Queue.full correspond à la taille maxsize
  • Queue.get([block[, timeout]]) Récupère l'élément de la file, timeout est le temps d'attente
  • Queue.get_nowait() Équivalent à Queue.get(False)
  • Queue.put(item, block=True, timeout=None) Écrit dans la file, timeout est le temps d'attente
  • Queue.put_nowait(item) Équivalent à Queue.put(item, False)
  • Queue.task_done() Après avoir terminé une tâche, la fonction Queue.task_done() envoie un signal à la file indiquant que la tâche a été accomplie.
  • Queue.join() Signifie en réalité attendre que la file soit vide avant d'effectuer d'autres opérations.

Exemple (Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import Queue import threading import time exitFlag = 0 class myThread (threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): print "Démarrage " + self.name process_data(self.name, self.q) print "Sortie " + self.name def process_data(threadName, q): while not exitFlag: queueLock.acquire() if not workQueue.empty(): data = q.get() queueLock.release() print "%s traitement %s" % (threadName, data) else: queueLock.release() time.sleep(1) threadList = ["Thread-1", "Thread-2", "Thread-3"] nameList = ["Un", "Deux", "Trois", "Quatre", "Cinq"] queueLock = threading.Lock() workQueue = Queue.Queue(10) threads = [] threadID = 1 # Créer un nouveau thread for tName in threadList: thread = myThread(threadID, tName, workQueue) thread.start() threads.append(thread) threadID += 1 # Remplir la file d'attente queueLock.acquire() for word in nameList: workQueue.put(word) queueLock.release() # Attendre que la file d'attente soit vidée while not workQueue.empty(): pass # Notifier les threads qu'il est temps de quitter exitFlag = 1 # Attendre que tous les threads se terminent for t in threads: t.join() print "Sortie du thread principal"

Le résultat de l'exécution du programme ci-dessus :

Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-1 processing One
Thread-2 processing Two
Thread-3 processing Three
Thread-1 processing Four
Thread-2 processing Five
Exiting Thread-3
Exiting Thread-1
Exiting Thread-2
Exiting Main Thread
Autres extensions