Retour à l'accueil

GIL CPython : fonctionnement et contournement senior

L'article décompose le GIL dans CPython : protection refcount, mécanisme de commutation, différences CPU/I/O-bound. Discute Lock/RLock, multiprocessing, libération du GIL dans les C extensions et asyncio.

GIL dans CPython : décomposition complète pour les entretiens seniors
Advertisement 728x90

Le GIL de CPython : Mécanisme, Limites et Contournements pour Développeurs Expérimentés

Dans CPython, le GIL protège le comptage de références des conditions de concurrence. Sans lui, deux threads pourraient décrémenter simultanément l'ob_refcnt du même objet : les deux verraient une valeur de 1, les deux libéreraient la mémoire — provoquant un segfault.

Termes clés :

  • Segfault : Accès à une mémoire libérée, déclenchant SIGSEGV.
  • Opcode : Une instruction de bytecode atomique (utilisez dis pour visualiser).
  • Comptage de références : Lorsque ob_refcnt tombe à 0, l'objet est détruit.
  • Mutex : Un seul thread peut entrer dans une section critique à la fois.
  • Condition de concurrence : Le résultat dépend de l'ordre d'exécution des threads.
  • Bytecode : Fichier .py compilé en .pyc, exécuté par la VM.
  • Changement de contexte : Surcharge d'environ ~microsecondes, perturbe le cache CPU.

Exemple d'une condition de concurrence sans le GIL :

Google AdInline article slot
# thread1 : refcount=1 -> free(obj)
# thread2 : refcount=1 -> free(obj) -> segfault

Objectif du GIL dans CPython

Le GIL est un mutex global qui permet à un seul thread d'exécuter du bytecode à la fois. Les threads peuvent travailler en parallèle pendant les appels système, mais pas dans le code Python.

Pourquoi il est nécessaire :

  • Protège ob_refcnt des modifications simultanées.
  • Simplifie les extensions C sans soucis de sécurité des threads.
  • Offre une sécurité des threads peu coûteuse sans synchronisation complexe.

Le GIL est spécifique à CPython. Jython et IronPython ne l'ont pas.

Google AdInline article slot

Mécanisme interne du GIL (depuis 3.2+)

Nouvelle implémentation dans Python/ceval_gil.c : la structure _gil_runtime_state.

Composants clés :

  • locked : Drapeau atomique (0/1).
  • mutex + cond_var : Primitives POSIX pour l'attente.
  • eval_breaker : Vérifié après chaque opcode.
  • gil_drop_request : Demande d'un thread en attente.

Algorithme :

Google AdInline article slot
  • Un thread détient le GIL et exécute du bytecode.
  • Après 5 ms (sys.getswitchinterval()), un autre thread signale.
  • Le thread actuel voit le drapeau via eval_breaker et libère le GIL.
  • Le thread en attente l'acquiert ; le thread actuel attend sur cond_var.

sys.setswitchinterval() : Par défaut 0,005 seconde. Augmentez à 0,05–0,1 seconde pour les tâches liées au CPU avec NumPy (moins de basculements). Ne l'ajustez pas pour les E/S.

Pourquoi Lock/RLock sont nécessaires malgré le GIL

Le GIL est atomique par opcode, mais counter += 1 implique LOAD_GLOBAL + BINARY_ADD + STORE_GLOBAL.

counter += 1
# Peut être interrompu après LOAD_GLOBAL
# Deux threads : +1 au lieu de +2

Solution :

import threading
counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    with lock:
        counter += 1

RLock pour la réentrance :

class SafeTree:
    def __init__(self):
        self._lock = threading.RLock()

    def insert(self, value):
        with self._lock:
            self._rebalance()  # pas de blocage

    def _rebalance(self):
        with self._lock:
            pass

Règle : Le GIL protège l'interpréteur ; Lock protège la logique métier.

Tâches liées au CPU vs liées aux E/S

Liées aux E/S (réseau/disque) : Le GIL est libéré pendant les appels système (lecture/écriture). threading/asyncio fournissent de la concurrence.

import threading, requests

def fetch(url):
    r = requests.get(url)  # GIL libre
    return r.status_code

Liées au CPU (calculs) : Le GIL bloque le parallélisme. Utilisez multiprocessing.

from multiprocessing import Pool

def heavy_compute(n):
    return sum(i * i for i in range(n))

with Pool(4) as p:
    results = p.map(heavy_compute, [10**7]*4)  # 4 GILs

Libération du GIL dans les extensions C

NumPy/pandas libèrent le GIL pendant les opérations lourdes :

Py_BEGIN_ALLOW_THREADS
matrix_multiply(a, b, result, n);
Py_END_ALLOW_THREADS

threading + NumPy = vrai parallélisme. Le Python pur dans les threads ne le permet pas.

asyncio et indépendance vis-à-vis du GIL

Multitâche coopératif dans un seul thread. Le changement se produit sur await.

import asyncio
import aiohttp

async def fetch_data(session, url):
    async with session.get(url) as resp:
        return await resp.json()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_data(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

threading vs asyncio pour les E/S :

  • threading : Threads du système d'exploitation, surcharge, conditions de concurrence.
  • asyncio : 1 thread, pas de surcharge, contrôle explicite.

Niveaux de sécurité Thread/Processus/Async

| Type | Problème | Mémoire partagée | Outils | Cas d'utilisation |

|------|---------|---------------|-------|----------|

| Thread | Concurrence dans les threads | Oui | Lock, Queue, local() | ThreadPoolExecutor |

| Processus | Concurrence entre processus | Explicitement | multiprocessing.Lock, Manager | Celery, Gunicorn |

| Async | Concurrence entre coroutines | Un seul thread | asyncio.Lock, éviter la mutabilité | FastAPI |

Exemple de concurrence asynchrone :

# PAS sûr
async def transfer(from_acc, to_acc, amount):
    balance = await db.get_balance(from_acc)
    if balance >= amount:  # yield ici
        await db.deduct(from_acc, amount)

Utilisez asyncio.Lock ou les transactions de base de données.

Points clés à retenir

  • Le GIL protège le comptage de références, pas la logique métier — utilisez toujours Lock.
  • Pour les tâches liées au CPU : multiprocessing, pas threading.
  • Les extensions C (NumPy) permettent le parallélisme dans les threads.
  • asyncio ignore le GIL : multitâche coopératif dans un seul thread.
  • sys.setswitchinterval() est rarement nécessaire, seulement pour les tâches intensives avec NumPy.

— Editorial Team

Advertisement 728x90

Lire ensuite