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.
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
ThreadPoolExecutorpour gérer des pools de threads - Privilégier
with lockpour 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.