50 · Procesos vs hilos: el modelo mental
Comprende la diferencia entre proceso e hilo, por qué el GIL de CPython hace que los hilos sean inútiles para trabajo CPU-bound (pero perfectos para I/O) y cómo elegir la herramienta correcta antes de escribir una sola línea de código concurrente.
Un **proceso** es un programa completamente aislado con su propia memoria — caro de crear, seguro para paralelizar. Un **hilo** comparte memoria dentro de un proceso — barato de crear, pero el Global Interpreter Lock (GIL) de CPython garantiza que solo un hilo ejecute bytecode Python a la vez. Esto hace que los hilos sean ideales para trabajo **I/O-bound** (esperando red, disco o base de datos) pero inútiles para trabajo CPU-bound (procesamiento numérico). Elegir la abstracción equivocada te cuesta horas de depuración con cero ganancia de rendimiento.
Sin esto:
Sin entender procesos vs hilos y el GIL, terminas usando `threading` para paralelizar un bucle de multiplicación de matrices y te preguntas por qué es *más lento* que el código secuencial — o usas `multiprocessing` para un web scraper y quemas CPU haciendo fork de docenas de procesos pesados para trabajo que unos pocos hilos manejarían trivialmente.
Dos unidades de ejecución
Antes de escribir una sola línea de Python concurrente necesitas dos palabras de vocabulario bien claras:
Proceso
Un proceso es una instancia de programa independiente. El sistema operativo le da:
- Su propio espacio de direcciones virtual — la memoria está completamente aislada de todos los demás procesos.
- Su propio intérprete de Python — incluyendo imports de módulos, variables globales y descriptores de archivo abiertos.
- Un PID real del OS —
os.getpid()te muestra el número.
Crear un nuevo proceso es costoso (~30–100 ms en Linux, más en Windows). Como la memoria está aislada, compartir datos entre procesos requiere serialización explícita a través de pipes, colas o segmentos de memoria compartida. La recompensa: verdadero paralelismo de CPU — dos procesos pueden ejecutar bytecode Python simultáneamente en dos núcleos de CPU.
Hilo
Un hilo (también llamado thread of execution) vive dentro de un proceso. Varios hilos comparten la misma memoria, los mismos imports de módulos y el mismo heap. Crear un hilo es barato (~0.1 ms). La advertencia crucial en CPython: el Global Interpreter Lock.
El GIL — una oración
CPython mantiene un mutex global llamado GIL mientras ejecuta bytecode Python, por lo que solo un hilo puede ejecutar bytecode Python en cualquier instante, incluso en una máquina multi-núcleo.
Esto significa que threading te da concurrencia (las tareas se intercalan) pero no paralelismo (las tareas corren simultáneamente en núcleos separados). Para trabajo I/O-bound — donde un hilo pasa la mayor parte del tiempo esperando una respuesta de red o lectura de disco — esto está bien: el hilo que espera libera el GIL y otro hilo corre. Para trabajo CPU-bound — bucles ajustados, matemáticas de arrays — el GIL convierte el threading en un cuello de botella secuencial.
PEP 703 — el futuro
PEP 703 ("Making the Global Interpreter Lock Optional") fue aceptado para CPython 3.13 como flag de compilación experimental. Durante la ventana 3.13–3.15, la compilación sin GIL se está volviendo mainstream. Por ahora (3.10–3.12, que es lo que usa la mayoría de los stacks de producción) el GIL todavía está en plena vigencia.
Latencia vs throughput
- I/O-bound — domina la latencia. Pasas tiempo esperando, no computando. Los hilos brillan aquí porque un hilo en espera libera el GIL.
- CPU-bound — domina el throughput. Pasas tiempo computando. El GIL hace que los hilos sean inútiles; usa
multiprocessingo vectorización numpy/extensión-C (ambos liberan el GIL a nivel C).
Dos procesos tienen heaps completamente aislados — sin estado compartido, sin problema del GIL, paralelismo real. Dos hilos en un proceso comparten el heap (genial para comunicación, peligroso sin locks) y compiten por el GIL (solo uno ejecuta Python a la vez).
Python (in browser)
Ejecuta esto y verás `thread count: 1` — solo el hilo principal. Todo programa Python comienza aquí. Toda la maquinaria que construirás en las próximas tres lecciones crece desde esta línea base de un solo hilo.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Pega este árbol de decisión en tu pared. El error más común: usar `threading` para acelerar matemáticas CPU. No funciona — el GIL lo garantiza. Primero perfila (`cProfile`, `line_profiler`), luego elige la herramienta correcta.
Necesitas procesar 10 millones de números de punto flotante usando un bucle puro de Python. ¿Acelerará esto `threading.Thread`?
- Un **proceso** tiene memoria aislada y un intérprete Python completo — caro de crear pero capaz de verdadero paralelismo de CPU. Un **hilo** comparte memoria dentro de un proceso — barato de crear pero limitado por el GIL en CPython.
- El **GIL** (Global Interpreter Lock) garantiza que solo un hilo ejecute bytecode Python a la vez. Se libera durante esperas de I/O y cómputo de extensiones C, por lo que los hilos SÍ son útiles para trabajo I/O-bound.
- Regla: **hilos para I/O-bound** (red, disco, DB), **procesos para CPU-bound** (cálculo numérico, procesamiento de imágenes, extracción de características).
- PEP 703 introduce una compilación de CPython sin GIL (3.13+) pero los stacks de producción mainstream (3.10–3.12) todavía tienen el GIL. Codifica en consecuencia.
- Todo programa Python comienza con exactamente un hilo (el hilo principal). Puedes inspeccionarlo con `threading.enumerate()`.
La carga de datos es I/O-bound — leer imágenes del disco, descargar batches desde almacenamiento de objetos — lo que la hace una carga de trabajo perfecta para hilos. El `DataLoader(num_workers=4)` de PyTorch genera 4 hilos (o procesos) trabajadores para precargar batches mientras la GPU está ocupada con el batch actual. La matemática real de numpy/tensor es a nivel C y libera el GIL, por lo que múltiples workers de DataLoader SÍ pueden correr en paralelo. La evaluación de folds de validación cruzada es Python CPU-bound y se beneficia de `multiprocessing`. La búsqueda de grilla (`GridSearchCV(n_jobs=-1)`) usa procesos internamente vía `joblib`.
Si lo quitas: Sin el modelo mental proceso/hilo aplicarás mal la concurrencia constantemente — poniendo en hilos un bucle de entrenamiento CPU-bound (sin aceleración, complejidad extra), o haciendo fork de un web scraper en 20 procesos (30× la memoria para trabajo que 4 hilos podrían hacer). El GIL es el hecho más malentendido de CPython, y equivocarse lleva a horas de profiling de un programa "paralelo" que corre idéntico al secuencial.