
Le GIL (Global Interpreter Lock) est un verrou global de l'interpréteur CPython qui autorise un seul thread à exécuter du bytecode Python à un instant donné. Conséquence directe et souvent mal comprise : lancer un calcul CPU-bound sur plusieurs threading.Thread ne le rend pas plus rapide, car les threads se relaient sur un unique cœur logique au lieu de tourner vraiment en parallèle. Ce guide explique pourquoi ce verrou existe (la protection du comptage de références de CPython), prouve son effet par la mesure, détaille les contournements réels (multiprocessing, extensions C qui relâchent le verrou, asyncio pour l'I/O) et fait le point sur le free-threading (PEP 703), le build sans GIL.
Il s'adresse aux développeurs qui profilent un programme, se demandent pourquoi leurs threads ne saturent pas les cœurs, et veulent choisir le bon modèle de concurrence. Les exemples ont été exécutés avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Définir ce qu'est le GIL et le rôle qu'il joue dans CPython.
- Comprendre pourquoi il protège le comptage de références.
- Prouver par la mesure que les threads ne parallélisent pas le CPU-bound.
- Choisir le bon contournement : processus, extension C ou asyncio.
- Situer le free-threading (PEP 703) et son statut actuel.
Qu'est-ce que le GIL
Section intitulée « Qu'est-ce que le GIL »Le GIL, ou Global Interpreter Lock, est un mutex unique protégeant l'ensemble de l'état interne de l'interpréteur CPython, l'implémentation de référence du langage. Un mutex (verrou d'exclusion mutuelle) garantit qu'une seule entité accède à une ressource à la fois. Ici, la ressource est l'interpréteur lui-même : pour exécuter la moindre instruction de bytecode Python, un thread doit d'abord acquérir le GIL, et il le relâche ensuite pour laisser sa chance aux autres.
La nuance capitale est que le GIL est un détail de CPython, pas une propriété du langage Python. D'autres implémentations comme Jython (sur la JVM) ou IronPython (sur .NET) n'ont jamais eu de GIL. Quand on parle du GIL, on parle donc du comportement de l'interpréteur que vous obtenez en tapant python3 sur la quasi-totalité des systèmes, celui distribué par python.org, les distributions Linux et les gestionnaires comme pyenv ou uv.
Pourquoi le GIL existe : le comptage de références
Section intitulée « Pourquoi le GIL existe : le comptage de références »CPython gère la mémoire par comptage de références (reference counting) : chaque objet garde un compteur du nombre de variables qui le référencent. Quand ce compteur tombe à zéro, l'objet est libéré immédiatement. Vous pouvez inspecter ce compteur avec sys.getrefcount, qui renvoie une valeur volontairement supérieure de un, car l'argument passé à la fonction compte lui-même comme une référence temporaire.
import sys
texte = "concurrence"print(sys.getrefcount(texte)) # p. ex. 2 : la variable + l'argument temporaireLe problème surgit dès que deux threads manipulent le même objet en même temps. Incrémenter ou décrémenter un compteur n'est pas une opération atomique au niveau machine : c'est une lecture, un calcul, puis une écriture. Sans protection, deux threads qui décrémentent simultanément peuvent écraser mutuellement leur résultat. Le compteur devient faux, et l'on tombe soit sur une fuite mémoire (objet jamais libéré), soit sur une libération prématurée (objet détruit alors qu'il est encore utilisé, source de corruption et de plantages).
Le GIL est la réponse historique et pragmatique de CPython à ce risque. Plutôt que de poser un verrou fin sur chaque objet, ce qui coûterait cher et compliquerait tout, l'interpréteur pose un seul verrou global. Tant qu'un thread le détient, aucun autre ne touche au comptage de références : la cohérence est garantie par construction. Ce choix rend aussi l'interpréteur mono-thread très rapide et simplifie énormément l'intégration d'extensions C, deux raisons pour lesquelles le GIL a survécu si longtemps.
La preuve par la mesure : les threads ne parallélisent pas le CPU
Section intitulée « La preuve par la mesure : les threads ne parallélisent pas le CPU »L'affirmation « les threads ne parallélisent pas le CPU-bound » se démontre en quelques lignes. On prend une fonction purement CPU-bound, c'est-à-dire qui ne fait que calculer sans jamais attendre le disque ou le réseau, et on compare deux exécutions séquentielles à deux threads concurrents.
import timeimport threadingimport sys
def compter(n: int) -> None: # Boucle purement CPU-bound : aucun accès disque ni reseau while n > 0: n -= 1
N = 50_000_000
# 1. Sequentiel : deux appels l'un apres l'autredebut = time.perf_counter()compter(N)compter(N)sequentiel = time.perf_counter() - debutprint(f"Sequentiel (2x) : {sequentiel:.2f} s")
# 2. Deux threads censes travailler "en parallele"debut = time.perf_counter()t1 = threading.Thread(target=compter, args=(N,))t2 = threading.Thread(target=compter, args=(N,))t1.start(); t2.start()t1.join(); t2.join()threads = time.perf_counter() - debutprint(f"2 threads : {threads:.2f} s")print(f"Acceleration threads : x{sequentiel / threads:.2f}")Voici la sortie obtenue sur une machine à 16 cœurs logiques, avec Python 3.12 :
Sequentiel (2x) : 1.37 s2 threads : 1.54 sAcceleration threads : x0.89Le résultat est sans appel. Avec deux cœurs réellement disponibles, on pourrait espérer une accélération proche de x2. On obtient x0,89, c'est-à-dire un programme légèrement plus lent en threads qu'en séquentiel. Le GIL sérialise les deux boucles sur un unique cœur, et l'on paie en plus le coût des changements de contexte et des allers-retours pour acquérir et relâcher le verrou. La machine a beau posséder seize cœurs, un seul travaille à un instant donné sur du bytecode Python.
Contourner le GIL avec multiprocessing
Section intitulée « Contourner le GIL avec multiprocessing »Puisque le verrou est par interpréteur, la parade la plus directe consiste à lancer plusieurs interpréteurs, c'est-à-dire plusieurs processus système. Chaque processus possède son propre GIL et sa propre mémoire ; ils tournent donc véritablement en parallèle sur des cœurs distincts. Le module multiprocessing de la bibliothèque standard fournit une API calquée sur threading pour cela.
import timefrom multiprocessing import Process
def compter(n: int) -> None: while n > 0: n -= 1
if __name__ == "__main__": N = 50_000_000 debut = time.perf_counter() p1 = Process(target=compter, args=(N,)) p2 = Process(target=compter, args=(N,)) p1.start(); p2.start() p1.join(); p2.join() print(f"2 processus : {time.perf_counter() - debut:.2f} s")Sur la même machine, le contraste est net : les deux processus bouclent en 0,91 s, contre 1,37 s pour la version séquentielle.
2 processus : 0.91 sL'accélération est réelle parce que chaque processus calcule sur son cœur sans se disputer un verrou commun. Ce gain a un prix qu'il faut connaître. Créer un processus est plus lourd qu'un thread, et surtout les processus ne partagent pas la mémoire : tout objet échangé doit être sérialisé (avec pickle) puis transmis, ce qui coûte du temps et interdit de passer des objets non picklables. Pour des tâches CPU nombreuses et indépendantes, on préfère en pratique un pool via concurrent.futures.ProcessPoolExecutor, qui recycle un nombre fixe de processus au lieu d'en créer un par tâche.
Contourner le GIL dans les extensions C et les bibliothèques numériques
Section intitulée « Contourner le GIL dans les extensions C et les bibliothèques numériques »Il existe une seconde issue, souvent invisible et pourtant décisive : une extension C peut relâcher volontairement le GIL pendant qu'elle exécute du code natif qui ne touche pas aux objets Python. Le code C encadre alors sa section de calcul par les macros Py_BEGIN_ALLOW_THREADS et Py_END_ALLOW_THREADS. Pendant cet intervalle, d'autres threads Python progressent librement, et le calcul natif s'exécute réellement en parallèle.
C'est exactement ce que font les grandes bibliothèques de calcul scientifique. NumPy, SciPy, pandas ou PyTorch délèguent leurs opérations lourdes à des routines compilées (souvent en C, C++ ou Fortran, via des back-ends comme BLAS) qui relâchent le GIL le temps du calcul. Un produit matriciel NumPy lancé sur plusieurs threads peut donc bel et bien saturer plusieurs cœurs, alors même que le GIL existe. Le verrou ne bloque que l'exécution de bytecode Python, pas le code natif sous-jacent.
Cette réalité change la lecture du problème. Si votre goulot d'étranglement est déjà vectorisé dans NumPy ou délégué à une bibliothèque native, le GIL n'est peut-être pas votre ennemi : le travail intensif se fait déjà hors de son emprise. Le GIL mord surtout sur du calcul écrit en Python pur, boucle par boucle, comme dans notre démonstration.
Contourner le GIL pour l'I/O avec asyncio
Section intitulée « Contourner le GIL pour l'I/O avec asyncio »Quand le programme passe l'essentiel de son temps à attendre (réponses réseau, requêtes de base de données, fichiers), le vrai besoin n'est pas le parallélisme mais la concurrence : occuper le temps d'attente utilement. C'est le terrain d'asyncio, le modèle asynchrone de la bibliothèque standard bâti sur une boucle d'événements et les mots-clés async et await.
Contrairement aux threads, asyncio fait tourner toutes les tâches sur un seul thread, donc sans lutte autour du GIL et sans changement de contexte système. À chaque await, une tâche qui attend rend la main à la boucle, qui fait avancer une autre tâche prête. Pour des milliers de connexions I/O simultanées, ce modèle est plus léger en mémoire et souvent plus prévisible que des milliers de threads.
Attention toutefois au piège symétrique : asyncio ne résout pas le CPU-bound. Une tâche qui calcule longuement sans await bloque toute la boucle d'événements, gelant les autres coroutines. Pour un calcul lourd au sein d'un programme asynchrone, on le déporte vers un pool de processus avec loop.run_in_executor. Le tableau ci-dessous résume quel outil viser selon la nature du travail.
| Nature du travail | Outil adapté | Le GIL est-il un frein ? |
|---|---|---|
| CPU-bound en Python pur | multiprocessing, ProcessPoolExecutor | Oui, contourné par des processus |
| CPU-bound vectorisé (NumPy, PyTorch) | Threads + extension native | Non, relâché par le code natif |
| I/O-bound (réseau, disque, base) | threading ou asyncio | Non, relâché pendant l'attente |
| I/O massif (milliers de connexions) | asyncio | Non, un seul thread suffit |
Le free-threading : un CPython sans GIL
Section intitulée « Le free-threading : un CPython sans GIL »Le chantier le plus marquant de l'écosystème est la suppression optionnelle du GIL, formalisée par la PEP 703 (« Making the Global Interpreter Lock Optional in CPython »). L'idée est de proposer un build free-threaded, dit « no-GIL », où plusieurs threads exécutent réellement du bytecode Python en parallèle, en remplaçant le verrou global par des mécanismes de synchronisation plus fins sur les objets et le ramasse-miettes.
Techniquement, il ne s'agit pas du même exécutable que le Python habituel. Le build free-threaded est séparé : son exécutable porte un suffixe t (par exemple python3.13t), et l'on peut vérifier l'état du verrou à l'exécution avec sys._is_gil_enabled(), qui renvoie False quand le GIL est désactivé. Cette fonction n'apparaît qu'avec Python 3.13 : sur un build classique 3.13 ou plus récent elle renvoie True, et sous Python 3.12, notre machine de test, l'appel échoue avec une AttributeError puisqu'elle n'existe pas encore. Le build free-threaded accepte aussi de réactiver le GIL au démarrage via la variable d'environnement PYTHON_GIL ou l'option -X gil.
Pièges courants autour du GIL
Section intitulée « Pièges courants autour du GIL »Le premier piège est de blâmer le GIL trop vite. Beaucoup de programmes « lents » ne sont pas CPU-bound : ils attendent une base de données ou un réseau. Là, le GIL n'est pas en cause, et passer à multiprocessing n'apporte rien, voire dégrade les choses par le coût de sérialisation. Profilez d'abord pour savoir si vous êtes I/O-bound ou CPU-bound avant de choisir une parade.
Le deuxième piège est de croire que le GIL rend le code automatiquement thread-safe. C'est faux. Le GIL protège l'état interne de l'interpréteur, pas la logique de votre programme. Une opération apparemment simple comme compteur += 1 sur une variable partagée reste non atomique au niveau du bytecode et peut se corrompre entre threads. Il faut toujours poser vos propres verrous (threading.Lock) sur les données partagées mutables. Le GIL et le free-threading ne changent rien à cette exigence de conception.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- Le GIL est un verrou global de CPython : un seul thread exécute du bytecode Python à la fois.
- Il existe pour protéger le comptage de références des objets contre les accès concurrents.
- Conséquence : les threads ne parallélisent pas le CPU-bound en Python pur (mesuré à x0,89 sur deux threads).
- Le GIL est relâché pendant l'I/O et dans les extensions C (NumPy, PyTorch), qui parallélisent réellement.
- Pour du CPU-bound en Python pur, on passe par des processus (
multiprocessing,ProcessPoolExecutor). - Pour de l'I/O massif,
asynciotient des milliers de connexions sur un seul thread. - Le free-threading (PEP 703, build
python3.13t) supprime le GIL ; vérifiez son statut pour votre version avant d'y bâtir dessus. - Le GIL ne rend pas votre code thread-safe : verrouillez vous-même les données partagées mutables.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- asyncio en Python : le modèle asynchrone pour occuper les temps d'attente sans multiplier les threads.
- FastAPI : un framework web bâti sur asyncio où le choix concurrence / parallélisme se pose concrètement.
- Type hints en Python : annoter proprement les fonctions de vos workers avant de les paralléliser.
- Le multiprocessing : contourner le verrou en répartissant le calcul sur plusieurs processus indépendants.
- Cython : relâcher le GIL avec
nogilpour paralléliser une boucle chaude dans une extension native.