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.
¿Qué es el Global Interpreter Lock?
Solo en CPythonEl 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
ejecutando
esperando
esperando
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 |
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
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
Loop
time.sleep, requests.get, un cálculo pesado)
dentro de una corrutina, bloqueás TODO el event loop.
| Aspecto | threading / asyncio |
|---|---|
| Modelo | Preemptivo / Cooperativo |
| Hilos | Varios del SO / Uno solo |
| Cambia de tarea | Cuando el SO quiere / Solo en await |
| GIL | Relevante / Irrelevante (1 hilo) |
| Locks | Necesarios / Casi nunca |
✅ 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())
❌ 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-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)
Cómo libera memoria Python
2 mecanismosPython 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
b = a # otra referencia al mismo objeto
del b
del a # nadie lo usa → se libera YA
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
revisión MUY frecuente
revisión media
revisión rara
🎯 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.