Qu'est-ce que le GIL en Python ?

Le Global Interpreter Lock (GIL) est utilisé par l'interpréteur CPython, empêchant plusieurs threads de s'exécuter en même temps. Ce mécanisme de verrouillage fait en sorte que même sur des processeurs multi-coeur, un seul thread est exécuté à la fois.

Il faut voir ça comme un verrou qui est acquis par un thread avant de s'exécuter. Une fois le verrou acquis par un thread, aucun autre thread ne peut s'exécuter jusqu'à ce que le premier thread le relâche.

Le GIL permet principalement de simplifier la gestion de la mémoire dans CPython. Par exemple, sans le GIL, plusieurs threads pourraient lire le même compteur de références d'un objet et l'incrémenter simultanément. Cela créerait des « race conditions » où une incrémentation serait perdue, causant des problèmes de gestion mémoire (fuites mémoire ou libération prématurée d'objets).

Dans l'exemple ci-dessous, imaginons qu'il n'y ait pas de GIL :

# SANS GIL - Race condition

compteur = 2

# Les deux threads lisent EN MÊME TEMPS
# Thread A                    # Thread B
temp_A = compteur  # lit 2    temp_B = compteur  # lit 2  
                              # ⚠️ Les deux lisent 2 simultanément !

compteur = temp_A + 1  # 3    compteur = temp_B + 1  # 3

# Résultat: 3 ❌ (devrait être 4)
PYTHON

Les deux threads lisent compteur = 2 au même moment, avant que l'un des deux n'ait eu le temps d'écrire la nouvelle valeur. Ils utilisent donc la même valeur de base (2) et calculent chacun de leur côté la nouvelle valeur, qui sera donc 3.

Avec le GIL un seul thread peut lire et modifier à la fois.

# AVEC GIL - Pas de problème
compteur = 2

# Thread A (a le GIL)
compteur = compteur + 1  # 3

# Thread B (obtient le GIL)
compteur = compteur + 1  # 4

# Résultat: 4 ✓
PYTHON

Comment gérer la concurrence du GIL ?

Tout dépend de ce que vous allez exécuter :

  • Des tâches CPU-bound qui sollicitent le processeur ?
    Comme des calculs complexes, du traitement d'images ou de vidéos, des simulations scientifiques etc.

  • Des tâches I/O-bound, c'est-à-dire des opérations d'entrée/sortie ?
    Comme des requêtes réseau, lecture/écriture sur des fichiers, etc.

Python permet d'utiliser 3 modules pour la concurrence du GIL, mais tous ne s'utilisent pas dans le même contexte.

Les tâches CPU-bound

Avant de lire cette partie, nous vous conseillons de lire notre glossaire sur multiprocessing. Ce module permet d'exécuter plusieurs processus en exploitant les processeurs multi-coeur.

import multiprocessing


def tache_intensive(nom) -> None:
    print(f"Début de {nom}")
    # Simulation d'un calcul intensif
    resultat = 0
    for i in range(5000000):
        resultat += i
    print(f"Fin de {nom}, résultat: {resultat}")


if __name__ == "__main__":

    # Création de 2 processus
    process1 = multiprocessing.Process(target=tache_intensive, args=("Tâche 1",))
    process2 = multiprocessing.Process(target=tache_intensive, args=("Tâche 2",))

    # Démarrage des processus
    print("Démarrage des processus")
    process1.start()
    process2.start()

    # Attendre la fin des processus
    process1.join()
    process2.join()
    print("Les processus sont terminés")
PYTHON

Les tâches I/O-bound

Nous vous conseillons de lire notre glossaire sur threading.
Les deux options les plus connues sont les modules threading et asyncio. threading permet de libérer le GIL pendant les attentes I/O. Pour des applications devant gérer un grand nombre de connexions, asyncio est préféré, car il permet de gérer la concurrence en un seul thread.

Voici un exemple simple avec threading :

import threading
import time


def tache_longue(nom: str) -> None:
    print(f"Début de {nom}")
    time.sleep(3)  # Simule une tâche qui prend du temps (comme une requête réseau)
    print(f"Fin de {nom}")


# Création de 2 threads
thread1 = threading.Thread(target=tache_longue, args=("Tâche 1",))
thread2 = threading.Thread(target=tache_longue, args=("Tâche 2",))

print("Lancement des threads...")

# Démarrage des threads - ils s'exécutent en parallèle
thread1.start()
thread2.start()

print("Les threads sont lancés et continuent en arrière-plan")

# Attente de la fin des threads
# Sans join(), le programme principal continuerait sans attendre la fin des threads
thread1.join()
thread2.join()

print("Tous les threads sont terminés")
PYTHON

Un autre exemple avec asyncio :

import asyncio

async def tache_longue(nom: str) -> None:
    print(f"Début de {nom}")
    await asyncio.sleep(3)  # Simule une tâche qui prend du temps
    print(f"Fin de {nom}")

async def main():
    print("Lancement des tâches...")

    # Création de 2 tâches qui s'exécutent de manière concurrente
    tache1 = asyncio.create_task(tache_longue("Tâche 1"))
    tache2 = asyncio.create_task(tache_longue("Tâche 2"))

    print("Les tâches sont lancées et continuent en arrière-plan")

    # Attente de la fin des tâches
    await tache1
    await tache2

    print("Toutes les tâches sont terminées")

# Point d'entrée
asyncio.run(main())
PYTHON

De manière générale asyncio est privilégié, sauf si les librairies utilisées ne sont pas compatibles.

À savoir qu'il est même possible d'utiliser des implémentations différentes de Python avec des interpréteurs comme Jython et IronPython. En effet, ces deux derniers n'ont pas de GIL.

L'avenir du GIL

Depuis Python 3.13 il est possible d'utiliser CPython en désactivant le GIL (voir PEP 703). Avec Python 3.14, les évolutions du GIL semblent continuer dans cette voie, mais nous n'en sommes qu'à une utilisation expérimentale.

Bravo, tu es prêt à passer à la suite

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.