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)
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 ✓
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")
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")
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())
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.