Initialisation des systèmes...

Baptiste.Dev
Retour aux notes
LangagesAvancéSérie : Python

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.

3 min de lecture
Mis à jour hier
pythongilthreadingmultiprocessingperformancecpu-bound

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âcheLe threading aide-t-il ?
CPU-boundNon. Les threads se relaient sous le GIL, aucun gain (souvent même une perte).
I/O-boundOui. 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)

# 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)

# 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