Python : le GIL et le multithreading
Pourquoi Python n'exécute qu'un thread à la fois, ce que veulent dire CPU-bound et I/O-bound, et comment on contourne la limite.
Python : le GIL et le multithreading
Le GIL (Global Interpreter Lock) est le point le plus mal compris de Python, et la source de sa réputation de langage « monothread ». Voici ce qu'il est vraiment, et ce que ça change.
# Qu'est-ce que le GIL ?
Le GIL est un verrou global dans CPython (l'implémentation de référence de Python) qui garantit qu'un seul thread exécute du bytecode Python à la fois. Même sur un processeur à 8 cœurs, deux threads Python ne calculent jamais vraiment en parallèle : ils se relaient, l'un après l'autre.
Ce n'est pas un oubli des concepteurs mais un choix : le GIL simplifie la gestion mémoire de l'interpréteur (le comptage de références) et rend le code monothread rapide, au prix du vrai parallélisme.
# CPU-bound vs I/O-bound
Pour comprendre l'impact du GIL, il faut classer le travail en deux familles :
- CPU-bound : la tâche est limitée par le processeur. Elle calcule (traitement d'image, chiffrement, boucle de nombres). Le CPU tourne à fond.
- I/O-bound : la tâche est limitée par l'attente d'une entrée/sortie. Elle patiente (requête réseau, lecture disque, appel à une base de données). Le CPU ne fait rien pendant ce temps.
La distinction est décisive, car le GIL ne les affecte pas pareil.
# L'effet du GIL, selon le type de tâche
| Type de tâche | Le threading aide-t-il ? |
|---|---|
| CPU-bound | Non. Les threads se relaient sous le GIL, aucun gain (souvent même une perte). |
| I/O-bound | Oui. Un thread relâche le GIL pendant qu'il attend l'I/O, laissant un autre avancer. |
Autrement dit : lancer 4 threads pour accélérer un gros calcul ne sert à rien en Python. Mais lancer 4 threads pour télécharger 4 fichiers en même temps fonctionne très bien, car chacun passe l'essentiel de son temps à attendre le réseau.
# I/O-bound : le threading est efficace (attente réseau)
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor() as executor:
resultats = executor.map(telecharger, urls)# I/O-bound : le threading est efficace (attente réseau)from concurrent.futures import ThreadPoolExecutorwith ThreadPoolExecutor() as executor: resultats = executor.map(telecharger, urls)# Comment on contourne pour le CPU-bound
Quand le travail est vraiment CPU-bound, on quitte les threads :
multiprocessing: lancer plusieurs processus au lieu de threads. Chaque processus a son propre interpréteur et son propre GIL, donc du vrai parallélisme sur plusieurs cœurs. Coût : la communication entre processus est plus lourde.- Bibliothèques natives : NumPy, pandas ou PyTorch font le gros du calcul en C, et relâchent le GIL pendant. C'est pour ça que le calcul scientifique en Python reste rapide malgré tout.
async/await: pour de la concurrence I/O massive (des milliers de connexions), l'asynchrone est plus léger que les threads.
# CPU-bound : le multiprocessing donne du vrai parallélisme
from multiprocessing import Pool
with Pool() as pool:
resultats = pool.map(calcul_lourd, donnees)# CPU-bound : le multiprocessing donne du vrai parallélismefrom multiprocessing import Poolwith Pool() as pool: resultats = pool.map(calcul_lourd, donnees)# Le GIL n'est pas éternel
Deux nuances importantes :
- Le GIL est un détail de CPython. D'autres implémentations (Jython, IronPython) n'en ont pas ; PyPy en a un mais optimise autrement.
- Depuis Python 3.13, une version expérimentale sans GIL (« free-threaded ») existe. Si elle se stabilise, le vrai multithreading CPU-bound deviendra possible nativement, ce qui changerait une des critiques historiques du langage.
La règle à retenir : threads pour l'I/O, processus (ou C) pour le CPU. Se tromper de côté est la cause n°1 de « mon threading Python n'accélère rien ».
# La suite
- Python : mon avis (mitigé) - où le GIL pèse dans la balance
- Python : Introduction - le hub de la série