Curso de Python / Comparar hilos, procesos y asyncio

Los hilos, procesos y asyncio permiten avanzar en varias tareas con modelos distintos. Elige según el trabajo de CPU, las esperas externas y las bibliotecas disponibles, y mide la carga real antes de afirmar que la concurrencia mejora el rendimiento.

Distinguir concurrencia y ejecución paralela

Modelo Funcionamiento Condiciones
Hilos Comparten memoria en un proceso Sincronización y comportamiento de bibliotecas
Procesos Tienen espacios de memoria separados Arranque, serialización y transferencia de datos
asyncio Avanza cooperativamente en puntos await Bibliotecas asíncronas y bucle sin bloqueos

En una compilación convencional de CPython con GIL, el bloqueo global del intérprete limita la ejecución simultánea de bytecode Python en hilos. Las extensiones que liberan el GIL y las compilaciones sin ese bloqueo cambian las condiciones. Considera conjuntamente intérprete, bibliotecas y carga.

Los hilos suelen ser una opción para entrada/salida bloqueante; los procesos, para cálculos Python que necesitan utilizar varios núcleos; asyncio, para entrada/salida con bibliotecas compatibles. Son criterios iniciales, no garantías de velocidad.

Comparar las interfaces con un programa completo

Guarda el código como concurrency.py y ejecuta python concurrency.py. Utiliza un archivo real: el arranque de procesos no tiene por qué comportarse igual en un cuaderno o sesión interactiva.

import asyncio
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def square(value):
    return value * value

async def async_square(value):
    await asyncio.sleep(0.01)
    return square(value)

async def gather_values():
    return await asyncio.gather(*(async_square(x) for x in [1, 2, 3]))

def main():
    with ThreadPoolExecutor(max_workers=2) as pool:
        print(list(pool.map(square, [1, 2, 3])))
    with ProcessPoolExecutor(max_workers=2) as pool:
        print(list(pool.map(square, [1, 2, 3])))
    print(asyncio.run(gather_values()))

if __name__ == "__main__":
    main()
[1, 4, 9]
[1, 4, 9]
[1, 4, 9]

Los tres modelos producen los mismos cuadrados. El ejemplo demuestra interfaces, no constituye un benchmark. El coste de iniciar procesos puede superar ampliamente este cálculo pequeño. map devuelve resultados en orden de entrada aunque las tareas terminen en otro orden.

Respetar puntos de entrada y cancelación

El inicio de procesos puede importar el módulo principal, especialmente en Windows. La protección de main evita un arranque recursivo y la función square definida a nivel de módulo queda disponible para los trabajadores.

Una función asíncrona con cálculo largo o llamadas síncronas sin ceder el control bloquea el bucle de eventos. asyncio.sleep demuestra una espera cooperativa; no realiza una petición HTTP. Guarda este segundo ejemplo como cancel.py y ejecútalo para observar la limpieza al cancelar:

import asyncio
async def worker():
    try:
        await asyncio.sleep(60)
    finally:
        print("worker cleanup")
async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(0)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("cancel confirmed")
if __name__ == "__main__":
    asyncio.run(main())
worker cleanup
cancel confirmed

Se ejecuta finally antes de que quien espera confirme la cancelación. No anides asyncio.run dentro de un bucle de eventos que ya esté en ejecución. La documentación de cancelación de tareas detalla su propagación.

Recoger errores y limitar trabajo pendiente

Guarda este programa como future_failure.py y ejecútalo con python future_failure.py:

from concurrent.futures import ThreadPoolExecutor
def calculate(value):
    if value < 0:
        raise ValueError("negative input")
    return value * value
with ThreadPoolExecutor(max_workers=2) as pool:
    futures = [pool.submit(calculate, value) for value in [2, -1]]
    for future in futures:
        try:
            print(future.result(timeout=5))
        except ValueError as error:
            print("failed:", error)
4
failed: negative input

Future.result propaga el fallo de la tarea. Su timeout limita la espera; no detiene por la fuerza una tarea que ya corre. max_workers limita trabajadores simultáneos, no toda la cola de envíos. Divide entradas grandes en lotes limitados y considera un Semaphore para limitar entrada/salida asíncrona.

La referencia de concurrent.futures explica esos límites. En el recopilador de API, comprende primero descarga y almacenamiento antes de paralelizar peticiones. Mide datos representativos, memoria y número de trabajadores; sincroniza el estado compartido y diseña propagación de errores y cierre de recursos.