concurrent.futures ofrece una API común para enviar funciones a pools de hilos o procesos. Los hilos suelen servir para trabajo que espera red o disco; los procesos pueden ayudar con tareas de CPU serializables y suficientemente grandes.

Recoger resultados al terminar

from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.request import urlopen


def tamano(url: str) -> tuple[str, int]:
    with urlopen(url, timeout=5) as respuesta:
        return url, len(respuesta.read(1_000_000))


urls = ["https://www.python.org/", "https://docs.python.org/3/"]
with ThreadPoolExecutor(max_workers=4) as executor:
    futuros = [executor.submit(tamano, url) for url in urls]
    for futuro in as_completed(futuros):
        try:
            print(futuro.result())
        except Exception as error:
            print(f"fallo: {error}")

Define timeouts también dentro de la operación. Agotar el tiempo al esperar un Future no detiene automáticamente una llamada de red. Limita entradas, respuestas y workers para no sobrecargar memoria o servicios externos.

Evita que una tarea espere otra del mismo pool cuando todos los workers están ocupados, porque puede producir un deadlock. En ProcessPoolExecutor, funciones y argumentos deben ser serializables y el punto de entrada necesita protección en plataformas que importan el módulo al crear procesos.

Para miles de conexiones cooperativas, async y await en Python pueden encajar mejor. La documentación oficial de concurrent.futures, consultada el 22 de julio de 2026, explica ejecutores, futures, cancelación y riesgos de deadlock.

Elegir según el trabajo medido

ThreadPoolExecutor encaja con funciones que esperan red, disco o base de datos. Los hilos comparten memoria, por lo que el estado mutable requiere sincronización. Python intensivo en CPU normalmente no obtiene paralelismo con hilos en CPython tradicional. ProcessPoolExecutor usa intérpretes separados y paga inicio y serialización. Mide el lote completo: procesos pueden perder con tareas pequeñas, mientras algunas bibliotecas nativas liberan el GIL.

Orden, resultados y excepciones

map() conserva el orden de entrada. submit() y as_completed() consumen por finalización y mantienen contexto:

items = [" Alfa ", "Beta", " gamma"]
with ThreadPoolExecutor(max_workers=3) as executor:
    pendientes = {executor.submit(str.strip, item): item for item in items}
    for future in as_completed(pendientes):
        original = pendientes[future]
        try:
            print(original, future.result())
        except Exception as exc:
            print(f"falló {original!r}: {exc}")

result() vuelve a lanzar la excepción. Observa todos los futures para no ocultar fallos parciales. Es mejor devolver un resultado inmutable que imprimir o mutar globales dentro del worker. El coordinador decide persistencia y reporte.

Limitar envío y presión

Pocos workers no limitan memoria si se crean millones de futures. Alimenta en ventanas, usa una cola acotada o buffering disponible. Ajusta el pool a la dependencia: cincuenta hilos no mejoran una base con diez conexiones.

Un timeout de espera no mata la operación. Red y base necesitan sus propios timeouts. Las tareas cooperativas pueden consultar threading.Event. cancel() solo funciona antes del inicio; no existe una forma segura de matar un hilo arbitrario. shutdown(cancel_futures=True) cancela lo que sigue en cola.

Procesos seguros

Protege el pool con if __name__ == "__main__":. Envía funciones de nivel superior y valores serializables. Lambdas, funciones anidadas, locks, archivos abiertos y clientes vivos suelen fallar.

def contar_divisores(numero: int) -> int:
    return sum(numero % d == 0 for d in range(1, numero + 1))

def main() -> None:
    with ProcessPoolExecutor(max_workers=2) as executor:
        print(list(executor.map(contar_divisores, [50_000, 50_001])))

if __name__ == "__main__":
    main()

Evita reenviar grandes datos. Inicializa estado local cuando corresponda y agrupa tareas pequeñas sin perjudicar balanceo.

Deadlocks y recursos

Un worker no debe esperar otro future en el mismo pool saturado. Mantén dependencias en el coordinador. No pases conexiones, clientes HTTP o transacciones a procesos. En hilos, confirma que el cliente sea thread-safe. Normalmente la transacción queda en el llamador, que valida todo antes del commit.

El pool limita simultaneidad, no peticiones por segundo. Un servicio con rate limit necesita limitador. Retries consumen workers, así que limita intentos y aplica backoff.

Observabilidad y pruebas

Mide espera en cola aparte de ejecución. Registra enviados, completados, fallidos y cancelados, con etiquetas acotadas. Pasa identificadores de correlación como valores serializables.

Prueba excepciones, timeout, finalización parcial, cancelación y cierre. No dependas del orden exacto salvo que sea el contrato. Usa servicios locales controlados. Executors no ofrecen colas durables ni trabajo distribuido: usa un sistema de jobs para sobrevivir reinicios y asyncio para muchas APIs cooperativas.

Comienza con código secuencial correcto, perfila e introduce concurrencia acotada. Propiedad, capacidad, política de error y cierre deben ser explícitos.

Modelar resultados

Un objeto de resultado inmutable puede incluir identificador, valor, duración y estado de dominio. El coordinador decide qué persistir después de revisar todo el lote. Así se reducen locks y se evita que mensajes intercalados sean el único registro. No captures Exception para devolver None, porque el llamador no distinguirá fallo de resultado vacío.

Si "no encontrado" es normal, represéntalo como estado esperado. Conserva excepciones para fallos inesperados y mantiene su cadena original. Asocia cada future con un identificador seguro, no con credenciales o datos personales.

Tamaño de bloques y equidad

Agrupar entradas reduce serialización en procesos, pero bloques enormes empeoran el reparto: un worker puede recibir todos los casos caros. Mide datos reales e irregulares. Dependencias con perfiles diferentes también pueden necesitar pools separados para que un servicio lento no agote toda la capacidad.

El executor básico no implementa prioridades. Si trabajos urgentes compiten con mantenimiento, usa capacidades separadas o un planificador. Mantén un presupuesto total de hilos, conexiones y memoria; crear varios pools no crea recursos gratuitos.

Inicialización de workers

Los procesos pueden crear estado local mediante un inicializador soportado. Es adecuado para datos de solo lectura o clientes que deben nacer dentro del proceso. No heredes una conexión abierta y no supongas que objetos globales tienen el mismo estado en cada worker. Define cierre y comportamiento cuando la inicialización falla.

En servidores, crea el executor durante startup y ciérralo durante shutdown. Instanciar un pool por petición añade latencia y multiplica hilos. Un pool compartido necesita backpressure para impedir que una sola ruta monopolice la cola.

Logs y métricas

Separa tiempo en cola de tiempo de ejecución. Una función rápida puede entregar tarde porque el pool está saturado. Métricas útiles incluyen pendientes, activas, completadas, fallidas, canceladas, espera y duración. Evita etiquetas con URLs completas o identificadores de usuario.

Los hilos comparten configuración de logging; los procesos tienen memoria independiente. Pasa correlación como argumento serializable y configura logging de workers conscientemente. No supongas que variables de contexto cruzan procesos.

Durabilidad y decisiones

Un future vive en memoria. Si el proceso termina, una tarea pendiente desaparece. Para emails, cobros o exportaciones que deben sobrevivir reinicios, registra la intención y usa una cola durable. El executor puede seguir aportando paralelismo local dentro de un worker, con límites.

Antes de producción, simula worker bloqueado, excepción intermedia, cierre y dependencia saturada. Verifica que el proceso termina limpiamente y que una parte fallida no parece un lote exitoso. Compara throughput y latencia con la versión secuencial. Si el beneficio no compensa operación y depuración, conserva la solución sencilla.