Le multithreading en Python permet d'exécuter plusieurs threads simultanément au sein d'un même processus. Cette technique est particulièrement utile pour les tâches I/O-bound (accès réseau, fichiers, etc.) et améliore la réactivité des applications. Voici un cours structuré sur le sujet :
Introduction au Multithreading
Les threads sont des unités d'exécution légères partageant la mémoire d'un processus. En Python, le module threading
facilite leur gestion :
import threading def tache(nom): print(f"Début {nom}") # Simulation de traitement print(f"Fin {nom}") thread1 = threading.Thread(target=tache, args=("Thread 1",)) thread2 = threading.Thread(target=tache, args=("Thread 2",)) thread1.start() thread2.start() thread1.join() thread2.join()
Le Global Interpreter Lock (GIL)
Le GIL limite l'exécution simultanée de threads Python sur plusieurs cœurs CPU : - CPU-bound : Peu de gains (exécution séquentielle) - I/O-bound : Gains significatifs (GIL libéré pendant les attentes)
Exemple CPU-bound :
import time def calcul_intensif(): sum(range(10**7)) # Single-thread start = time.time() calcul_intensif() calcul_intensif() print(f"Single: {time.time() - start:.2f}s") # Multi-thread start = time.time() t1 = threading.Thread(target=calcul_intensif) t2 = threading.Thread(target=calcul_intensif) t1.start(); t2.start() t1.join(); t2.join() print(f"Threads: {time.time() - start:.2f}s") # Temps similaire !
Synchronisation avec les Verrous
Pour éviter les race conditions lors de l'accès aux ressources partagées :
compteur = 0 verrou = threading.Lock() def incrementer(): global compteur for _ in range(100000): with verrou: compteur += 1 threads = [threading.Thread(target=incrementer) for _ in range(2)] for t in threads: t.start() for t in threads: t.join() print(f"Compteur final: {compteur}") # 200000
Cas d'Usage Courants
Téléchargements parallèles :
import requests def download(url): response = requests.get(url) print(f"Téléchargé {url} ({len(response.content)} octets)") urls = ["https://example.com"] * 5 threads = [threading.Thread(target=download, args=(url,)) for url in urls] for t in threads: t.start() for t in threads: t.join()
Interfaces Graphiques Réactives :
import tkinter as tk import time def long_task(): time.sleep(3) label.config(text="Tâche terminée") root = tk.Tk() label = tk.Label(root, text="En attente...") label.pack() threading.Thread(target=long_task).start() root.mainloop()
Gestion des Deadlocks
Les deadlocks surviennent quand des threads s'attendent mutuellement. Solution avec timeout :
verrou1 = threading.Lock() verrou2 = threading.Lock() def tache1(): with verrou1: time.sleep(0.1) if verrou2.acquire(timeout=1): # Opération verrou2.release() def tache2(): with verrou2: time.sleep(0.1) if verrou1.acquire(timeout=1): # Opération verrou1.release()
Alternatives au Multithreading
Pour les tâches CPU-bound, préférer le multiprocessing :
import multiprocessing def calcul(n): return sum(range(n)) with multiprocessing.Pool() as pool: print(pool.map(calcul, [10**7, 10**7])) # Utilise tous les cœurs
Bonnes Pratiques
- Utiliser
ThreadPoolExecutor
pour gérer des pools de threads - Privilégier
with lock
pour la gestion automatique des verrous - Éviter les variables globales, préférer les queues (
queue.Queue
) - Toujours joindre les threads avec
join()
Conclusion
Le multithreading Python excelle pour les tâches I/O-bound mais est limité par le GIL pour le calcul intensif. Maîtrisez les mécanismes de synchronisation et combinez avec le multiprocessing quand nécessaire.