Le module `threading` dans Python

Le module `threading` dans Python

Cet article explique le module threading dans Python.

YouTube Video

Le module threading dans Python

Le module threading dans Python est une bibliothèque standard qui prend en charge la programmation multithreadée. L'utilisation des threads permet à plusieurs processus de s'exécuter simultanément, ce qui peut améliorer les performances des programmes, en particulier dans les cas impliquant des opérations bloquantes comme les attentes d'E/S. En raison du Global Interpreter Lock (GIL) de Python, l'efficacité du multithreading est limitée pour les opérations liées au processeur, mais il fonctionne efficacement pour les opérations liées aux E/S.

Les sections suivantes expliquent les bases de l'utilisation du module threading et comment contrôler les threads.

Utilisation de base des threads

Création et exécution de threads

Pour créer un thread et effectuer un traitement simultané, utilisez la classe threading.Thread. Spécifiez la fonction cible pour créer un thread et exécuter ce thread.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10# Create and start the thread
11thread = threading.Thread(target=worker)
12thread.start()
13
14# Processing in the main thread
15print("Main thread continues to run")
16
17# Wait for the thread to finish
18thread.join()
19print("Main thread finished")
  • Dans cet exemple, la fonction worker est exécutée dans un thread séparé, tandis que le thread principal continue de fonctionner. En appelant la méthode join(), le thread principal attend que le sous-thread se termine.

Nommer les threads

Donner des noms significatifs aux threads facilite la journalisation et le débogage. Vous pouvez le spécifier avec l’argument name.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10t = threading.Thread(
11    target=worker,
12    args=("named-worker", 0.3),
13    name="MyWorkerThread"
14)
15
16t.start()
17
18print("Active threads:", threading.active_count())
19for th in threading.enumerate():
20    print(" -", th.name)
21
22t.join()
  • threading.enumerate() retourne une liste des threads en cours, ce qui est utile pour le débogage et le suivi de l’état.

Hériter de la classe Thread

Si vous souhaitez personnaliser la classe exécutant le thread, vous pouvez définir une nouvelle classe en héritant de la classe threading.Thread.

 1import threading
 2import time
 3
 4# Inherit from the Thread class
 5class WorkerThread(threading.Thread):
 6    def __init__(self, name, delay, repeat=3):
 7        super().__init__(name=name)
 8        self.delay = delay
 9        self.repeat = repeat
10        self.results = []
11
12    def run(self):
13        for i in range(self.repeat):
14            msg = f"{self.name} step {i+1}"
15            print(msg)
16            self.results.append(msg)
17            time.sleep(self.delay)
18
19# Create and start the threads
20t1 = WorkerThread("Worker-A", delay=0.4, repeat=3)
21t2 = WorkerThread("Worker-B", delay=0.2, repeat=5)
22
23t1.start()
24t2.start()
25
26t1.join()
27t2.join()
28
29print("Results A:", t1.results)
30print("Results B:", t2.results)
  • Dans cet exemple, la méthode run() est surchargée pour définir le comportement du thread, permettant à chaque thread de conserver ses propres données. Ceci est utile lorsque les threads effectuent un traitement complexe ou lorsque vous souhaitez que chaque thread possède ses propres données indépendantes.

Synchronisation entre les threads

Lorsque plusieurs threads accèdent simultanément à des ressources partagées, des conflits de données peuvent survenir. Pour éviter cela, le module threading propose plusieurs mécanismes de synchronisation.

Verrou (Lock)

L'objet Lock est utilisé pour mettre en œuvre un contrôle exclusif des ressources entre les threads. Lorsqu'un thread verrouille une ressource, les autres threads ne peuvent pas y accéder.

 1import threading
 2
 3lock = threading.Lock()
 4shared_resource = 0
 5
 6def worker():
 7    global shared_resource
 8    with lock:  # Acquire the lock
 9        local_copy = shared_resource
10        local_copy += 1
11        shared_resource = local_copy
12
13threads = [threading.Thread(target=worker) for _ in range(5)]
14
15for t in threads:
16    t.start()
17
18for t in threads:
19    t.join()
20
21print(f"Final value of shared resource: {shared_resource}")
  • Dans cet exemple, cinq threads accèdent à une ressource partagée, mais le Lock est utilisé pour empêcher plusieurs threads de modifier les données simultanément.

Verrou réentrant (RLock)

Si le même thread doit acquérir un verrou plusieurs fois, utilisez un RLock (verrou réentrant). Ceci est utile pour les appels récursifs ou les appels de bibliothèque susceptibles d’acquérir des verrous lors d’appels différents.

 1import threading
 2
 3rlock = threading.RLock()
 4shared = []
 5
 6def outer():
 7    with rlock:
 8        shared.append("outer")
 9        inner()
10
11def inner():
12    with rlock:
13        shared.append("inner")
14
15t = threading.Thread(target=outer)
16t.start()
17t.join()
18print(shared)
  • Avec RLock, le même thread peut réacquérir un verrou qu’il détient déjà, ce qui aide à éviter les interblocages lors d’acquisitions de verrous imbriquées.

Condition (Condition)

Condition est utilisé pour que les threads attendent jusqu'à ce qu'une condition spécifique soit remplie. Lorsqu’un thread satisfait une condition, vous pouvez appeler notify() pour avertir un autre thread, ou notify_all() pour avertir tous les threads en attente.

Ci-dessous se trouve un exemple de producteur et consommateur utilisant une Condition.

 1import threading
 2
 3condition = threading.Condition()
 4shared_data = []
 5
 6def producer():
 7    with condition:
 8        shared_data.append(1)
 9        print("Produced an item")
10        condition.notify()  # Notify the consumer
11
12def consumer():
13    with condition:
14        condition.wait()  # Wait until the condition is met
15
16        item = shared_data.pop(0)
17        print(f"Consumed an item: {item}")
18
19# Create the threads
20producer_thread = threading.Thread(target=producer)
21consumer_thread = threading.Thread(target=consumer)
22
23consumer_thread.start()
24producer_thread.start()
25
26producer_thread.join()
27consumer_thread.join()
  • Ce code utilise une Condition pour que le producteur notifie lorsque des données sont ajoutées, et le consommateur attend cette notification avant de récupérer les données, permettant ainsi la synchronisation.

Daemonisation des threads

Les threads daemon sont terminés de force lorsque le thread principal se termine. Alors que les threads normaux doivent attendre pour se terminer, les threads daemon se terminent automatiquement.

 1import threading
 2import time
 3
 4def worker():
 5    while True:
 6        print("Working...")
 7        time.sleep(1)
 8
 9# Create a daemon thread
10thread = threading.Thread(target=worker)
11thread.daemon = True  # Set as a daemon thread
12
13thread.start()
14
15# Processing in the main thread
16time.sleep(3)
17print("Main thread finished")
  • Dans cet exemple, le thread worker est daemonisé, donc il est terminé de force lorsque le thread principal se termine.

Gestion des threads avec ThreadPoolExecutor

En plus du module threading, vous pouvez utiliser le ThreadPoolExecutor du module concurrent.futures pour gérer un pool de threads et exécuter des tâches en parallèle.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def worker(seconds):
 5    print(f"Sleeping for {seconds} second(s)")
 6    time.sleep(seconds)
 7    return f"Finished sleeping for {seconds} second(s)"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(worker, i) for i in range(1, 4)]
11    for future in futures:
12        print(future.result())
  • ThreadPoolExecutor crée un pool de threads et traite efficacement les tâches. Spécifiez le nombre de threads à exécuter simultanément avec max_workers.

Communication événementielle entre threads

En utilisant threading.Event, vous pouvez définir des indicateurs entre les threads pour notifier d'autres threads de la survenue d'un événement.

 1import threading
 2import time
 3
 4event = threading.Event()
 5
 6def worker():
 7    print("Waiting for event to be set")
 8    event.wait()  # Wait until the event is set
 9
10    print("Event received, continuing work")
11
12thread = threading.Thread(target=worker)
13thread.start()
14
15time.sleep(2)
16print("Setting the event")
17event.set()  # Set the event and notify the thread
  • Ce code illustre un mécanisme où le thread de travail attend le signal Event et reprend le traitement lorsque le thread principal appelle event.set().

Gestion des exceptions et terminaison des threads

Lorsque des exceptions surviennent dans les threads, elles ne sont pas propagées directement au thread principal, il est donc nécessaire d’adopter une méthode pour capturer et partager les exceptions.

 1import threading
 2import queue
 3
 4def worker(err_q):
 5    try:
 6        raise ValueError("Something bad")
 7    except Exception as e:
 8        err_q.put(e)
 9
10q = queue.Queue()
11t = threading.Thread(target=worker, args=(q,))
12t.start()
13t.join()
14if not q.empty():
15    exc = q.get()
16    print("Worker raised:", exc)
  • En plaçant les exceptions dans une Queue et en les récupérant dans le thread principal, vous pouvez détecter les échecs de manière fiable. Si vous utilisez concurrent.futures.ThreadPoolExecutor, les exceptions sont relancées par future.result(), ce qui facilite leur gestion.

Le GIL (Global Interpreter Lock) et ses effets

En raison du mécanisme du GIL (Global Interpreter Lock) dans CPython, plusieurs instructions Python ne s’exécutent pas réellement simultanément dans un même processus. Pour les tâches intensives en calcul, telles que les calculs complexes, il est recommandé d’utiliser multiprocessing. En revanche, pour les tâches soumises à l’I/O, comme la lecture de fichiers ou la communication réseau, threading fonctionne efficacement.

Résumé

Avec le module threading de Python, vous pouvez implémenter des programmes multithreadés et exécuter plusieurs processus simultanément. Avec des mécanismes de synchronisation comme Lock et Condition, vous pouvez accéder en toute sécurité à des ressources partagées et effectuer une synchronisation complexe. De plus, en utilisant des threads daemon ou ThreadPoolExecutor, la gestion des threads et le traitement parallèle efficace deviennent plus simples.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video