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.