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

  1. Utiliser ThreadPoolExecutor pour gérer des pools de threads
  2. Privilégier with lock pour la gestion automatique des verrous
  3. Éviter les variables globales, préférer les queues (queue.Queue)
  4. 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.