Python3 Multithreading

Le multithreading est semblable à l'exécution simultanée de plusieurs programmes différents. L'exécution multithread 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 montrer la progression du traitement.
  • La vitesse d'exécution du programme peut augmenter.
  • 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 et la réception de données réseau. Dans de tels cas, nous pouvons libérer certaines ressources précieuses comme l'utilisation de la mémoire.

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

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

Les registres du pointeur d'instruction et du pointeur de pile sont les deux registres les plus importants dans le contexte d'un thread. Les threads s'exécutent toujours dans le contexte obtenu par le processus, et ces adresses servent à marquer 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 mis en attente (aussi appelé sommeil) -- c'est le fait de céder le thread.

Les threads peuvent être divisés en :

  • Threads noyau :Créés et révoqués par le noyau du système d'exploitation.
  • Threads utilisateur :Threads implémentés dans des programmes utilisateur sans nécessiter de support du noyau.

Deux modules couramment utilisés pour les threads dans Python3 sont :

  • _thread
  • threading (recommandé)

Le module thread a été déprécié. Les utilisateurs peuvent utiliser le module threading à la place. Par conséquent, dans Python3, le module "thread" ne peut plus être utilisé. Pour des raisons de compatibilité, Python3 a renommé thread en "_thread".


Commencez à apprendre les threads Python

Il existe deux façons d'utiliser les threads en Python : des fonctions ou l'utilisation de classes pour encapsuler des objets thread.

Style fonctionnel : appelez la fonction start_new_thread() du module _thread pour générer 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 paramètres passés à la fonction du thread ; il doit être de type tuple.
  • kwargs - Paramètres optionnels.

Exemple

#!/usr/bin/python3

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: Wed Jan  5 17:38:08 2022
Thread-2: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:12 2022
Thread-2: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:16 2022
Thread-2: Wed Jan  5 17:38:18 2022
Thread-2: Wed Jan  5 17:38:22 2022
Thread-2: Wed Jan  5 17:38:26 2022

Après avoir exécuté le programme ci-dessus, vous pouvez appuyer sur ctrl-c pour quitter.


Module Thread

Python3 prend en charge les threads via deux bibliothèques standard : _thread et threading.

_thread fournit des threads de bas niveau et primitifs ainsi qu'un verrou simple. Ses fonctionnalités sont encore relativement limitées par rapport au module threading.

En plus de contenir toutes les méthodes du module _thread, le module threading fournit également les méthodes suivantes :

  • threading.current_thread(): renvoie la variable du thread courant.
  • threading.enumerate(): renvoie une liste contenant tous les threads en cours d'exécution. « En cours d'exécution » signifie après que le thread a démarré et avant qu'il n'ait terminé, sans inclure les threads avant le démarrage ou après la terminaison.
  • threading.active_count(): renvoie le nombre de threads en cours d'exécution, avec le même résultat que len(threading.enumerate()).
  • threading.Thread(target, args=(), kwargs={}, daemon=None):
    • CréezThreadinstance de la classe.
    • target: la fonction cible que le thread doit exécuter.
    • args: les paramètres de la fonction cible, passés sous forme de tuple.
    • kwargs: les arguments nommés de la fonction cible, passés sous forme de dictionnaire.
    • daemon: spécifie si le thread est un thread démon.

La classe threading.Thread fournit les méthodes et attributs suivants :

  1. __init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):

    • InitialisationThreadobjet.
    • group: groupe de threads, actuellement inutilisé, réservé à une extension future.
    • target: la fonction cible que le thread doit exécuter.
    • name: le nom du thread.
    • args: les paramètres de la fonction cible, passés sous forme de tuple.
    • kwargs: les arguments nommés de la fonction cible, passés sous forme de dictionnaire.
    • daemon: spécifie si le thread est un thread démon.
  2. start(self):

    • Démarrez le thread. Il appellera la méthode du threadrun()méthode.
  3. run(self):

    • Le thread définit le code à exécuter dans cette méthode.
  4. join(self, timeout=None):

    • Attendez que le thread se termine. Par défaut,join()il se bloquera jusqu'à ce que le thread appelé se termine. Si letimeoutparamètre est spécifié, il attend au plustimeoutsecondes.
  5. is_alive(self):

    • Renvoie si le thread est en cours d'exécution. Si le thread a démarré et ne s'est pas encore terminé, il renvoieTrue, sinon elle retourneFalse。
  6. getName(self):

    • Renvoie le nom du thread.
  7. setName(self, name):

    • Définit le nom du thread.
  8. identAttributs :

    • L'identifiant unique du thread.
  9. daemonAttributs :

    • L'indicateur daemon du thread, utilisé pour indiquer s'il s'agit d'un thread daemon.
  10. isDaemon()Méthodes :

Un exemple simple de thread :

Exemple

import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(i)

# Créer un thread
thread = threading.Thread(target=print_numbers)

# Démarrer le thread
thread.start()

# Attendre la fin du thread
thread.join()

Le résultat de sortie est:

0
1
2
3
4

Créer des threads à l'aide du module threading

Nous pouvons créer une nouvelle sous-classe en héritant directement de threading.Thread, puis, après instanciation, appeler la méthode start() pour démarrer le nouveau thread. C'est-à-dire qu'elle appelle la méthode run() du thread :

Exemple

#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("Démarrage du thread :" + self.name)
        print_time(self.name, self.delay, 5)
        print ("Sortie du thread :" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# Créer un nouveau thread
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# Démarrer le nouveau thread
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Sortie du thread principal")

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

开始线程:Thread-1
开始线程:Thread-2
Thread-1: Wed Jan  5 17:34:54 2022
Thread-2: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:56 2022
Thread-2: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:58 2022
退出线程:Thread-1
Thread-2: Wed Jan  5 17:34:59 2022
Thread-2: Wed Jan  5 17:35:01 2022
Thread-2: Wed Jan  5 17:35:03 2022
退出线程:Thread-2
退出主线程

Synchronisation des threads

Si plusieurs threads modifient ensemble un certain élément de données, des résultats inattendus peuvent se produire. Afin de garantir l'exactitude des données, plusieurs threads doivent être synchronisés.

L'utilisation de Lock et Rlock des objets Thread permet d'obtenir une synchronisation simple des threads. Les deux objets ont des méthodes acquire et release. Pour les données sur lesquelles un seul thread est autorisé à opérer à la fois, les 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). Mais lorsque les threads doivent partager des données, il peut y avoir un problème de désynchronisation des données.

Considérez une situation : tous les éléments d'une liste sont 0. Le thread "set" change tous les éléments de l'arrière vers l'avant en 1, tandis que le thread "print" lit la liste de l'avant vers l'arrière et l'imprime.

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, le concept de verrous est introduit.

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 le thread "set" est mis en pause, ce qui est un blocage synchrone ; après que le thread "print" a fini d'accéder et libère le verrou, le thread "set" continue.

Avec un tel traitement, lors de l'impression de la liste, soit tous les 0, soit tous les 1 sont affichés, et la scène gênante de moitié 0 et moitié 1 n'apparaîtra plus.

Exemple

#!/usr/bin/python3

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("Démarrer le thread : " + self.name)
        # Acquérir le verrou pour la synchronisation des threads
        threadLock.acquire()
        print_time(self.name, self.delay, 3)
        # Libérer le verrou pour démarrer le thread suivant
        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 le nouveau thread
thread1.start()
thread2.start()

# Ajouter les threads à la liste des threads
threads.append(thread1)
threads.append(thread2)

# Attendre que tous les threads soient terminés
for t in threads:
    t.join()
print ("Sortie du thread principal")

Exécutez le programme ci-dessus, et le résultat de sortie est :

开启线程: Thread-1
开启线程: Thread-2
Thread-1: Wed Jan  5 17:36:50 2022
Thread-1: Wed Jan  5 17:36:51 2022
Thread-1: Wed Jan  5 17:36:52 2022
Thread-2: Wed Jan  5 17:36:54 2022
Thread-2: Wed Jan  5 17:36:56 2022
Thread-2: Wed Jan  5 17:36:58 2022
退出主线程

File d'attente prioritaire de threads (Queue)

Le module Queue de Python fournit des classes de files d'attente synchronisées et sûres pour les threads, notamment la file FIFO (premier entré, premier sorti) Queue, la file LIFO (dernier entré, premier sorti) LifoQueue et la file prioritaire PriorityQueue.

Ces files d'attente implémentent toutes des primitives de verrouillage et peuvent être utilisées directement en multithreading. Les files d'attente peuvent être utilisées pour assurer la synchronisation entre les threads.

Méthodes courantes du module Queue :

  • Queue.qsize() renvoie la taille de la file d'attente.
  • Queue.empty() Si la file d'attente est vide, renvoie True ; sinon False
  • Queue.full() Si la file d'attente est pleine, renvoie True ; sinon False
  • Queue.full correspond à la taille de maxsize
  • Queue.get([block[, timeout]]) Obtient une file d'attente ; timeout est le temps d'attente
  • Queue.get_nowait() Équivalent à Queue.get(False)
  • Queue.put(item) Écrit dans la file d'attente ; 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 d'attente indiquant que la tâche a été terminée
  • Queue.join() Signifie en fait attendre que la file d'attente soit vide, puis effectuer d'autres opérations

Exemple

#!/usr/bin/python3

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 du thread :" + self.name)
        process_data(self.name, self.q)
        print ("Sortie du thread :" + self.name)

def process_data(threadName, q):
    while not exitFlag:
        queueLock.acquire()
        if not workQueue.empty():
            data = q.get()
            queueLock.release()
            print ("%s traite %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éation d'un nouveau thread
for tName in threadList:
    thread = myThread(threadID, tName, workQueue)
    thread.start()
    threads.append(thread)
    threadID += 1

# Remplissage de la file d'attente
queueLock.acquire()
for word in nameList:
    workQueue.put(word)
queueLock.release()

# En attente que la file d'attente soit vidée
while not workQueue.empty():
    pass

# Notification au thread qu'il est temps de quitter
exitFlag = 1

# En attente 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 :

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程
Autres extensions