Maîtriser le Multithreading Python : GIL Expliqué avec Exemples Pratiques
Python offre à la fois multiprocessing et multithreading pour accélérer les charges de travail. Ce guide vous accompagne dans la création d'applications robustes basées sur les threads, les nuances du Global Interpreter Lock (GIL), et comment éviter les pièges courants comme les deadlocks et race conditions.
Qu'est-ce qu'un Thread ?
Un thread est une unité d'exécution légère au sein d'un seul processus. Les threads partagent l'espace mémoire du processus, permettant un échange de données efficace, tandis que le système d'exploitation les planifie pour s'exécuter concurremment sur un ou plusieurs cœurs CPU.
Qu'est-ce qu'un Processus ?
Un processus est une instance de programme indépendante. Quand vous lancez une application — disons un navigateur ou éditeur de texte — l'OS crée un processus séparé avec son propre code, données, et map mémoire.
Fondamentaux du Multithreading Python
Le module threading de Python fournit une API haut niveau pour gérer les threads, alors que le module legacy _thread est déprécié et seulement conservé pour compatibilité ascendante. Les threads sont idéaux pour tâches I/O-bound, tandis que le travail CPU-bound devrait utiliser le module multiprocessing pour contourner le GIL.
- Thread : mémoire partagée, léger.
- Processus : mémoire isolée, plus lourd.
- GIL : un mutex protégeant l'exécution du bytecode Python.
Quand Utiliser le Multithreading
Le multithreading brille quand vous devez effectuer plusieurs opérations I/O-lourdes — requêtes réseau, I/O fichiers, ou appels base de données — simultanément. Des threads bien conçus peuvent améliorer réactivité et débit sans la surcharge de spawn de nouveaux processus.
Modules Python Clés pour le Threading
- _thread — Bas niveau, déprécié.
- threading — Moderne, riche en fonctionnalités, et choix recommandé.
Utilisation du Module threading
Le module threading offre une classe Thread, primitives de synchronisation (Lock, RLock, Semaphore, Condition, Event, Barrier), et fonctions utilitaires comme activeCount() et enumerate(). Voici un exemple minimal démontrant création et synchronisation de threads.
import time
import _thread
def thread_test(name, wait):
for i in range(4):
time.sleep(wait)
print(f"Running {name}")
print(f"{name} a terminé son exécution")
if __name__ == "__main__":
_thread.start_new_thread(thread_test, ("First Thread", 1))
_thread.start_new_thread(thread_test, ("Second Thread", 2))
_thread.start_new_thread(thread_test, ("Third Thread", 3))
L'exécution de ce code produit une sortie entrelacée démontrant l'exécution concurrente.

Explication
- Importer
timeet le module legacy_thread. - Définir
thread_testpour effectuer travail et logger progression. - Lancer trois threads indépendants via
start_new_thread.
Threading Moderne avec threading.Thread
En pratique, vous sous-classerez threading.Thread et surchargerez sa méthode run(). L'exemple suivant miroite le comportement précédent mais utilise l'API haut niveau.
import time
import threading
class ThreadTester(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
for _ in range(4):
time.sleep(self.delay)
print(f"Running {self.name}")
print(f"{self.name} a terminé son exécution")
if __name__ == "__main__":
threads = [
ThreadTester("First Thread", 1),
ThreadTester("Second Thread", 2),
ThreadTester("Third Thread", 3),
]
for t in threads:
t.start()
for t in threads:
t.join()
Sortie :

Explication
- Sous-classer
Threadpour encapsuler comportement thread. - Surcharger
__init__pour stocker données thread-spécifiques. - Surcharger
runpour définir la tâche. - Instancier, démarrer, et joindre chaque thread.
Pièges Courants du Threading
Deadlocks
Un deadlock se produit quand deux ou plus threads attendent indéfiniment des ressources détenues par les autres. Illustration classique : le problème des Philosophes Dînant — cinq philosophes compétitionnent pour cinq fourchettes, chacun en ayant besoin de deux pour manger. Si chaque philosophe prend la fourchette de droite d'abord, une attente circulaire surgit, stoppant tout progrès.
Race Conditions
Quand plusieurs threads modifient des données partagées sans coordination appropriée, l'état final peut être imprévisible. Par exemple :
i = 0
for _ in range(100):
print(i)
i += 1
Lancer cette boucle en threads parallèles peut produire une séquence brouillée car les incréments se chevauchent.
Synchroniser les Threads
Le module threading fournit plusieurs primitives de synchronisation. La plus fondamentale est Lock, qui assure un accès exclusif à une ressource partagée.
import threading
lock = threading.Lock()
def worker(name):
for _ in range(5):
lock.acquire()
print(f"{name} a acquis le lock")
# section critique
lock.release()
if __name__ == "__main__":
t1 = threading.Thread(target=worker, args=("Thread A",))
t2 = threading.Thread(target=worker, args=("Thread B",))
t1.start(); t2.start()
t1.join(); t2.join()
La sortie démontre l'accès sérialisé à la section critique.

Autres Outils de Synchronisation
- RLock — lock ré-entrant.
- Semaphore — contrôle accès à nombre fini de ressources.
- Condition — permet threads d'attendre conditions spécifiques.
- Event — drapeau simple pour signalisation inter-thread.
- Barrier — bloque un ensemble de threads jusqu'à ce que tous atteignent un point.
Comprendre le Global Interpreter Lock (GIL)
CPython utilise le GIL pour sérialiser l'exécution du bytecode à travers les threads. Bien que cela protège les structures internes de l'interpréteur (ex: comptage de références) des race conditions, cela limite aussi le parallélisme pour tâches CPU-bound sur systèmes multi-cœurs.
- Quand un thread démarre, il acquiert le GIL.
- Si un autre thread a besoin du GIL, il bloque jusqu'à ce que le premier le libère.
- Les threads I/O-bound libèrent le GIL pendant appels bloquants, permettant aux autres de s'exécuter.
- Les threads CPU-bound gardent le GIL, empêchant autres threads d'exécuter du code Python.
Par conséquent, le multithreading en Python apporte peu de bénéfice pour charges de calcul intensives ; multiprocessing est l'approche préférée dans ces cas.
Pourquoi le GIL Existe
Le ramasse-miettes CPython repose sur le comptage de références. Sans verrou global, mises à jour concurrentes des comptes de références mèneraient à race conditions et corruption mémoire. Un verrou par objet introduirait une surcharge significative, donc le GIL a été adopté comme solution simple et efficace.
Points Pratiques à Retenir
- Utilisez
threadingpour concurrence I/O-bound. - Préférez
multiprocessingpour parallélisme CPU-bound. - Synchronisez toujours données partagées avec locks ou primitives haut niveau.
- Soyez conscient de l'impact du GIL lors du scaling across cores.
- Sous-classez
Threadet surchargezrun()pour code propre et maintenable.
Python
- Maîtriser la fonction free() en C : Guide pratique et exemple
- Maîtriser la méthode strip() de Python : Guide complet et exemples pratiques
- Maîtriser str.count() de Python : Compter caractères et sous-chaînes avec exemples
- Fonction Python round() expliquée avec exemples pratiques
- Maîtriser la fonction Python map() : syntaxe, exemples et bonnes pratiques
- Python timeit() : mesurer le temps d'exécution avec exemples pratiques
- Python Counter dans collections : comptage efficace, mise à jour et opérations arithmétiques
- Python list.count() : Guide Expert avec Exemples Pratiques
- Python list.index() : Comment Trouver les Positions d'Éléments avec Exemples Pratiques