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
workerest exécutée dans un thread séparé, tandis que le thread principal continue de fonctionner. En appelant la méthodejoin(), 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
Lockest 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
Conditionpour 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
workerest 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())ThreadPoolExecutorcrée un pool de threads et traite efficacement les tâches. Spécifiez le nombre de threads à exécuter simultanément avecmax_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
Eventet reprend le traitement lorsque le thread principal appelleevent.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
Queueet en les récupérant dans le thread principal, vous pouvez détecter les échecs de manière fiable. Si vous utilisezconcurrent.futures.ThreadPoolExecutor, les exceptions sont relancées parfuture.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.