Concurrencia: Hilos y Multiprocesamiento
Python ofrece diferentes modelos para ejecutar código de forma simultánea o paralela según la naturaleza del problema:
- Tareas intensivas en Entrada/Salida (I/O-Bound): Espera por red, disco o peticiones HTTP.
- Tareas intensivas en Cálculo/CPU (CPU-Bound): Procesamiento de imágenes, algoritmos pesados, matemáticas o compresión.
1. El GIL (Global Interpreter Lock) #
En la implementación estándar de Python (CPython), existe el GIL (Global Interpreter Lock), un mecanismo de bloqueo que asegura que solo un hilo ejecute bytecode de Python a la vez, incluso en procesadores con múltiples núcleos.
- Para I/O-Bound: Los hilos liberan el GIL mientras esperan operaciones de red/disco, por lo que
threadinges muy efectivo. - Para CPU-Bound: El GIL impide el paralelismo real de hilos en CPU. Para aprovechar múltiples núcleos en cálculos pesados, se debe recurrir al módulo
multiprocessing(procesos independientes, cada uno con su propia memoria y su propio intérprete).
💡 Python 3.13+: Incorpora soporte experimental para compilar CPython sin GIL (Free-threaded mode), permitiendo paralelismo real con hilos en CPU.
2. Hilos con ThreadPoolExecutor (I/O-Bound) #
El módulo estándar concurrent.futures ofrece una interfaz de alto nivel mediante pools de trabajadores:
1import time 2from concurrent.futures import ThreadPoolExecutor 3 4def descargar_url(url: str) -> str: 5 print(f"Descargando {url}...") 6 time.sleep(1.0) # Simula latencia de red 7 return f"Contenido de {url}" 8 9urls = ["https://api1.com", "https://api2.com", "https://api3.com"] 10 11inicio = time.perf_counter() 12with ThreadPoolExecutor(max_workers=3) as executor: 13 resultados = list(executor.map(descargar_url, urls)) 14 15duracion = time.perf_counter() - inicio 16print(f"Descargas finalizadas en {duracion:.2f} segundos")
3. Multiprocesamiento con ProcessPoolExecutor (CPU-Bound) #
Para distribuir cálculos pesados a través de todos los núcleos del procesador:
1import time 2from concurrent.futures import ProcessPoolExecutor 3 4def calculo_pesado(n: int) -> int: 5 # Simula cálculo intensivo en CPU 6 return sum(i * i for i in range(n)) 7 8if __name__ == "__main__": 9 cargas = [5_000_000, 5_000_000, 5_000_000, 5_000_000] 10 11 inicio = time.perf_counter() 12 with ProcessPoolExecutor() as executor: 13 resultados = list(executor.map(calculo_pesado, cargas)) 14 15 duracion = time.perf_counter() - inicio 16 print(f"Cálculos en paralelo finalizados en {duracion:.2f} segundos")
4. Cuadro Comparativo: ¿Cuándo usar cada modelo? #
| Modelo | Módulo | Tipo de Tarea | Sobrecarga | Paralelismo Real de CPU |
|---|---|---|---|---|
Asincronía (asyncio) | asyncio | I/O masivo (miles de conexiones) | Muy baja (un solo hilo) | No |
Hilos (threading) | concurrent.futures.ThreadPoolExecutor | I/O moderado con código bloqueante | Media | No (limitado por GIL) |
Procesos (multiprocessing) | concurrent.futures.ProcessPoolExecutor | CPU pesado (machine learning, render, matemáticas) | Alta (memoria separada) | Sí (1 proceso por núcleo) |
Resumen del tema
Conceptos clave #
- GIL (Global Interpreter Lock): mecanismo de CPython que restringe la ejecución de bytecode a un solo hilo a la vez.
- I/O-Bound vs CPU-Bound:
- I/O-Bound: usar
asynciooThreadPoolExecutorpara no bloquear mientras se espera red o disco. - CPU-Bound: usar
ProcessPoolExecutor(multiprocessing) para saltar el GIL y usar el 100% de los núcleos físicos.
- I/O-Bound: usar
concurrent.futures: interfaz estándar y elegante basada en gestores de contexto (with) para orquestar pools de hilos o procesos.
Qué debes recordar #
Usa asyncio o ThreadPoolExecutor para operaciones de red y disco, y ProcessPoolExecutor para procesamiento intensivo de datos y cálculos pesados en CPU.