Article

GIL, Concurrencia y Garbage Collection en Python

Published June 08, 2026 · 14 hours, 46 minutes ago

Gastón Gaitan
Gastón Gaitan
Backend Engineer · Jun 08, 2026
GIL, Concurrencia y Garbage Collection en Python
GIL, Concurrencia y Garbage Collection en Python
⚡ Python Internals · Concurrencia · Memoria

GIL, Concurrencia y Garbage Collection en Python

Tres temas que casi siempre aparecen en entrevistas de Python backend: cómo funciona el GIL, qué modelo de concurrencia elegir (threading, multiprocessing o asyncio) y cómo Python administra la memoria con su garbage collector.

🔒 El GIL

Un lock de CPython: solo un hilo ejecuta bytecode a la vez por proceso.

🚀 Concurrencia

CPU-bound → multiprocessing.
I/O-bound → threading o asyncio.

🧹 Memoria

Reference counting para casi todo + GC generacional para romper ciclos.

GIL

¿Qué es el Global Interpreter Lock?

Solo en CPython

El GIL es un mutex de la implementación CPython (no del lenguaje en sí) que garantiza que solo un hilo ejecute bytecode de Python a la vez por proceso. Existe en parte porque el reference counting no es thread-safe: sin el GIL, dos hilos podrían corromper los contadores.

Un solo carril: los hilos se turnan el GIL

Thread 1
ejecutando
🔒 GIL
Thread 2
esperando
Thread 3
esperando
Clave: CPython hace context-switch cada cierto intervalo (sys.setswitchinterval, ~5 ms) y libera el GIL durante operaciones de I/O y en muchas extensiones en C (NumPy, pandas).
Tipo de tarea¿El GIL molesta?
CPU-bound (cálculo)Sí: serializa, no hay paralelismo con threads
I/O-bound (red, disco, DB)No: el GIL se libera durante la espera
Extensiones C (NumPy)A veces no: liberan el GIL internamente
Dato actual: Python 3.13 introdujo un modo experimental free-threaded (PEP 703) que permite compilar CPython sin GIL.
Concurrencia

Los 3 modelos: cuándo usar cada uno

threading · multiprocessing · asyncio

🧵 threading

  • I/O-bound
  • Varios hilos, 1 proceso
  • Memoria compartida
  • Librerías bloqueantes (requests)
  • Necesita locks

⚙️ multiprocessing

  • CPU-bound
  • Varios procesos
  • Cada uno con su GIL
  • Paralelismo real
  • Más costo de memoria/IPC

⚡ asyncio

  • I/O-bound masivo
  • 1 hilo, 1 event loop
  • Cooperativo (await)
  • Librerías async (httpx)
  • Muy liviano

📊 Qué modelo elegir según la carga

Cálculo pesado (CPU)
multiproc
Miles de requests HTTP
asyncio
Lib bloqueante (requests)
threading
Lectura/escritura de archivos
threading
Procesar imágenes/ML
multiproc
threading vs asyncio

Preemptivo vs Cooperativo

La diferencia clave

Los dos sirven para I/O, pero manejan la concurrencia de forma opuesta. threading es preemptivo: el intérprete interrumpe los hilos cuando quiere. asyncio es cooperativo: el control solo cambia en los await.

asyncio: un solo hilo, un event loop que salta entre corrutinas

Event
Loop
task_a()  await db.query()
task_b()  await http.get()
task_c()  await sleep(1)
Trampa clásica: si metés una operación bloqueante (time.sleep, requests.get, un cálculo pesado) dentro de una corrutina, bloqueás TODO el event loop.
Aspectothreading / asyncio
ModeloPreemptivo / Cooperativo
HilosVarios del SO / Uno solo
Cambia de tareaCuando el SO quiere / Solo en await
GILRelevante / Irrelevante (1 hilo)
LocksNecesarios / Casi nunca
asyncio_ok.py

✅ Concurrencia real con librería async

httpx + gather: las 3 requests viajan a la vez.

import asyncio
import httpx


async def fetch(client, url):
    r = await client.get(url)          # cede el control en el await
    return r.status_code


async def main():
    async with httpx.AsyncClient() as client:
        urls = ["https://a.com", "https://b.com", "https://c.com"]
        results = await asyncio.gather(
            *(fetch(client, u) for u in urls)
        )
    print(results)


asyncio.run(main())
asyncio_trap.py

❌ El error que mata el event loop

requests es bloqueante: envolverlo en async NO da concurrencia.

import asyncio
import requests   # librería SÍNCRONA / bloqueante


async def fetch(url):
    # esto BLOQUEA el event loop entero:
    return requests.get(url).status_code


# Solución correcta: sacarlo a un thread
async def fetch_ok(url):
    return await asyncio.to_thread(
        lambda: requests.get(url).status_code
    )
cpu_multiprocessing.py

⚙️ CPU-bound: paralelismo real con procesos

Cada proceso tiene su propio GIL → usa todos los cores.

from multiprocessing import Pool


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


if __name__ == "__main__":
    with Pool(processes=4) as pool:
        results = pool.map(heavy, [10_000_000] * 4)
    print(results)
Garbage Collection

Cómo libera memoria Python

2 mecanismos

Python combina reference counting (el principal) con un garbage collector generacional (el respaldo) que existe solo para resolver lo que el primero no puede: las referencias circulares.

1️⃣ Reference Counting — se libera al llegar a 0

a = [1, 2, 3]  # crea la lista
1
b = a  # otra referencia al mismo objeto
2
del b
1
del a  # nadie lo usa → se libera YA
0
cycle.py

El problema: referencias circulares

El refcount nunca llega a 0 → memory leak sin el GC.

a = {}
b = {}
a["ref"] = b      # a apunta a b
b["ref"] = a      # b apunta a a  (ciclo)

del a
del b             # las variables se borran, pero los dicts
                  # se siguen apuntando entre sí -> refcount != 0

import gc
gc.collect()      # el GC cíclico detecta y libera el ciclo

2️⃣ GC Generacional — los objetos jóvenes se revisan más seguido

Gen 0
objetos nuevos
revisión MUY frecuente
Gen 1
sobrevivieron
revisión media
Gen 2
longevos
revisión rara
Por qué generacional: la mayoría de los objetos “mueren jóvenes”. Revisar seguido la Gen 0 (barato) y rara vez la Gen 2 (cara) hace al GC mucho más eficiente.
Conexión con el GIL: el reference counting no es thread-safe, y el GIL es justamente lo que evita que dos hilos corrompan los contadores a la vez. Por eso ambos temas suelen ir juntos en la entrevista.

🎯 Resumen

  • GIL Lock de CPython: 1 hilo de bytecode por proceso. Se libera en I/O.
  • threading I/O-bound con librerías bloqueantes. Preemptivo, necesita locks.
  • multiprocessing CPU-bound: varios procesos, cada uno con su GIL → paralelismo real.
  • asyncio I/O-bound masivo. Un hilo, cooperativo: cambia solo en los await.
  • GC Reference counting + GC generacional para romper ciclos.