Objetivo: ejecutar el mismo conjunto de tareas con threading, multiprocessing y asyncio, midiendo cómo se comportan en 100 tareas I/O-bound (simuladas con sleep) y 100 tareas CPU-bound (cálculo de primos).
Se evalúa el entendimiento práctico del GIL, la medición rigurosa y la interpretación de ventajas/desventajas reales.
Los tres modelos ejecutan el mismo trabajo con diferente semántica de concurrencia. El GIL impide que hilos escalen en CPU-bound, pero no afecta tareas I/O-bound. Los procesos eluden el GIL y el event loop de asyncio brilla en espera de red/temporizadores.
asyncio.sleep).matplotlib para comparar barras.grafico_rendimiento.png.Guarda el siguiente script como comparador.py. Requiere solo la biblioteca estándar; el gráfico usa matplotlib si está instalado (pip install matplotlib).
import asyncio
import concurrent.futures as futures
import math
import os
import time
from typing import Awaitable, Callable, Iterable, List, Tuple
TAREAS_IO = 100
TAREAS_CPU = 50
SLEEP_S = 0.05 # 50 ms para simular I/O
MAX_CPU = os.cpu_count() or 4
RANGO_PRIMOS = range(10_000, 50_000)
def io_sleep(_):
time.sleep(SLEEP_S)
return True
def es_primo(n: int) -> bool:
if n < 2:
return False
if n % 2 == 0:
return n == 2
limite = int(math.sqrt(n)) + 1
for i in range(3, limite, 2):
if n % i == 0:
return False
return True
def cpu_primos(rango: Iterable[int]) -> int:
return sum(1 for n in rango if es_primo(n))
def medir(nombre: str, fn: Callable[[], None]) -> Tuple[str, float]:
inicio = time.perf_counter()
fn()
return nombre, time.perf_counter() - inicio
def medir_async(nombre: str, coro: Callable[[], Awaitable[None]]) -> Tuple[str, float]:
inicio = time.perf_counter()
asyncio.run(coro())
return nombre, time.perf_counter() - inicio
def medir_io_threading():
with futures.ThreadPoolExecutor(max_workers=20) as ex:
list(ex.map(io_sleep, range(TAREAS_IO)))
def medir_io_multiprocessing():
with futures.ProcessPoolExecutor(max_workers=MAX_CPU) as ex:
list(ex.map(io_sleep, range(TAREAS_IO)))
async def medir_io_asyncio():
await asyncio.gather(*[asyncio.sleep(SLEEP_S) for _ in range(TAREAS_IO)])
def medir_cpu_threading():
with futures.ThreadPoolExecutor(max_workers=MAX_CPU) as ex:
list(ex.map(cpu_primos, [RANGO_PRIMOS] * TAREAS_CPU))
def medir_cpu_multiprocessing():
with futures.ProcessPoolExecutor(max_workers=MAX_CPU) as ex:
list(ex.map(cpu_primos, [RANGO_PRIMOS] * TAREAS_CPU))
async def medir_cpu_asyncio():
loop = asyncio.get_running_loop()
# Usa hilos por defecto: el GIL impide paralelizar CPU, solo se interlelan.
with futures.ThreadPoolExecutor(max_workers=MAX_CPU) as pool:
tareas = [loop.run_in_executor(pool, cpu_primos, RANGO_PRIMOS) for _ in range(TAREAS_CPU)]
await asyncio.gather(*tareas)
def ejecutar_pruebas() -> List[Tuple[str, float]]:
resultados = []
resultados.append(medir("io_threading", medir_io_threading))
resultados.append(medir("io_multiprocessing", medir_io_multiprocessing))
resultados.append(medir_async("io_asyncio", medir_io_asyncio))
resultados.append(medir("cpu_threading", medir_cpu_threading))
resultados.append(medir("cpu_multiprocessing", medir_cpu_multiprocessing))
resultados.append(medir_async("cpu_asyncio_threads", medir_cpu_asyncio))
return resultados
def imprimir_tabla(resultados: List[Tuple[str, float]]):
print("\n=== Resultados (segundos) ===")
for nombre, t in resultados:
print(f"{nombre:22s} -> {t:0.3f}s")
def graficar(resultados: List[Tuple[str, float]], ruta: str = "grafico_rendimiento.png"):
try:
import matplotlib.pyplot as plt
except ImportError:
print("Instala matplotlib para generar el gr\u00e1fico: pip install matplotlib")
return
etiquetas = [r[0] for r in resultados]
tiempos = [r[1] for r in resultados]
colores = ["#4caf50" if "io" in e else "#ff9800" for e in etiquetas]
plt.figure(figsize=(10, 5))
plt.bar(etiquetas, tiempos, color=colores)
plt.xticks(rotation=25, ha="right")
plt.ylabel("segundos (menos es mejor)")
plt.title("Threading vs Multiprocessing vs Asyncio")
plt.tight_layout()
plt.savefig(ruta, dpi=120)
print(f"Gr\u00e1fico guardado en {ruta}")
if __name__ == "__main__":
resultados = ejecutar_pruebas()
imprimir_tabla(resultados)
graficar(resultados)
Puntos clave:
cpu_threading y cpu_asyncio_threads no hay paralelismo real; el tiempo suele parecerse al secuencial.cpu_multiprocessing escala al número de núcleos; con el rango 10 000-50 000 el costo de spawn se amortiza y debería ser menor que hilos.io_asyncio y io_threading completan rápido al solapar esperas; io_multiprocessing paga overhead sin ganar.matplotlib está instalado, se genera grafico_rendimiento.png; de lo contrario se imprime la tabla.asyncio) es el más eficiente en número de hilos; threading también escala aceptablemente; procesos son innecesarios.asyncio con hilos solo intercalan ejecución.asyncio o hilos; mezcla modelos cuando el trabajo es mixto.perf_counter.asyncio en I/O.