concurrent.futures: hilos y procesos en paralelo

Publicado el: 26/08/2026
Tempo de leitura: 7 minutos
Bright yellow and blue shopping carts arranged in orderly rows outdoors.

El módulo concurrent.futures ofrece una interfaz de alto nivel para ejecutar funciones de manera concurrente. En lugar de crear y coordinar threads o procesos manualmente, una aplicación envía llamadas a un executor y recibe objetos Future que representan resultados que todavía pueden no estar listos. El mismo modelo sirve para executors basados en threads y procesos.

La API simplifica colas, recopilación de resultados, propagación de excepciones, timeouts, cancelación y shutdown. No elimina las dificultades de la concurrencia: todavía debes elegir el executor correcto, limitar trabajo pendiente, evitar deadlocks, proteger estado compartido y decidir cómo una falla afecta al resto de la operación.

Executor y Future

Un Executor acepta trabajo. submit() devuelve un Future. El objeto permite consultar estado, esperar, obtener un valor, observar una excepción o cancelar trabajo que aún no comenzó.

from concurrent.futures import ThreadPoolExecutor


def cuadrado(numero):
    return numero * numero

with ThreadPoolExecutor(max_workers=4) as executor:
    futuro = executor.submit(cuadrado, 12)
    print(futuro.result())

El bloque with llama a shutdown() al terminar y aplica la política normal de espera. Esto evita pools abandonados.

ThreadPoolExecutor

ThreadPoolExecutor ejecuta threads dentro del mismo proceso. Suele ser adecuado para operaciones que pasan gran parte del tiempo esperando I/O: HTTP, archivos, bases de datos, subprocesses, brokers y APIs externas.

Las threads comparten memoria, por lo que pasar objetos es sencillo, pero el estado mutable compartido crea condiciones de carrera. Usa queues, locks, datos inmutables o reglas de ownership.

Trabajo limitado por CPU

Para funciones que ejecutan bytecode Python continuamente, varias threads normalmente no ofrecen paralelismo completo en múltiples núcleos en builds tradicionales del intérprete. Un pool de procesos puede encajar mejor.

Extensiones nativas que liberan el lock del intérprete pueden escalar con threads. Mide el workload real.

ProcessPoolExecutor

ProcessPoolExecutor usa procesos separados y puede distribuir trabajo de CPU entre varios núcleos. Argumentos y resultados deben ser serializables, y las funciones enviadas deben poder importarse en los workers.

from concurrent.futures import ProcessPoolExecutor


def calcular(numero):
    return sum(i * i for i in range(numero))

if __name__ == "__main__":
    with ProcessPoolExecutor() as executor:
        futuros = [executor.submit(calcular, n) for n in (10000, 20000, 30000)]
        print([f.result() for f in futuros])

La protección if __name__ == "__main__" es esencial en plataformas que inician workers importando nuevamente el módulo principal.

Elige la cantidad de workers

Más workers no implican automáticamente más rendimiento. Demasiadas threads aumentan context switching, conexiones y presión sobre servicios externos. Demasiados procesos aumentan memoria, serialización, startup y competencia por CPU.

Basa el límite en el recurso más restringido: núcleos, pool de base, rate limits, ancho de banda, memoria, descriptores o capacidad del sistema downstream.

Envía tareas individuales

submit(funcion, *args, **kwargs) ofrece control sobre cada tarea.

with ThreadPoolExecutor(max_workers=8) as executor:
    futuros = {
        executor.submit(descargar, url, timeout=10): url
        for url in urls
    }

Relacionar futuros con sus entradas facilita diagnóstico y retries selectivos.

Usa map para transformaciones ordenadas

executor.map() aplica una función a varias entradas y devuelve resultados en el orden de entrada.

with ThreadPoolExecutor(max_workers=4) as executor:
    resultados = list(executor.map(procesar, items))

El orden es cómodo, pero una tarea lenta puede retrasar resultados posteriores ya terminados. Usa as_completed() cuando prefieras orden de finalización.

Consume con as_completed

as_completed() produce futuros a medida que terminan.

from concurrent.futures import as_completed

for futuro in as_completed(futuros):
    origen = futuros[futuro]
    try:
        resultado = futuro.result()
    except Exception as error:
        registrar_fallo(origen, error)
    else:
        guardar(resultado)

Una excepción del worker vuelve a ser lanzada por result() en la thread o proceso que recopila el resultado.

Espera grupos

wait() puede esperar todas las tareas, la primera finalizada o la primera excepción. Devuelve conjuntos de futuros concluidos y pendientes.

from concurrent.futures import wait, FIRST_EXCEPTION

concluidos, pendientes = wait(futuros, return_when=FIRST_EXCEPTION)

Después de una falla, decide explícitamente si el trabajo pendiente debe continuar, cancelarse o terminar para mantener consistencia.

Timeouts

future.result(timeout=...), wait() y as_completed() limitan cuánto espera el caller. Un timeout de espera no detiene automáticamente la función que ya está en ejecución.

from concurrent.futures import TimeoutError

try:
    valor = futuro.result(timeout=5)
except TimeoutError:
    registrar_aviso("la tarea superó el límite de espera")

La propia tarea debe aplicar timeouts a sockets, bases, subprocesses y APIs para poder finalizar.

Cancelación

cancel() funciona solo antes de que una tarea comience. cancelled() informa el éxito y running() indica que la ejecución empezó.

Detener trabajo ya iniciado requiere cooperación con un Event, deadline, token o señal compartida.

def procesar(items, detener):
    for item in items:
        if detener.is_set():
            return None
        ejecutar_paso(item)

Shutdown

shutdown(wait=True) cierra el executor y normalmente espera tareas enviadas. Cancelar futuros pendientes puede ser útil durante una falla o el cierre de la aplicación.

No dejes indefinida la vida de un executor global. Establece quién lo crea, quién lo cierra y qué ocurre con operaciones incompletas.

Deadlocks en pools de threads

Puede ocurrir un deadlock cuando una tarea espera otra del mismo pool y todos los workers ya están ocupados.

def tarea_a():
    return futuro_b.result()

Evita dependencias bloqueantes entre tareas del mismo executor. Coordina secuencias fuera del pool, usa callbacks o combina resultados en la thread que envía.

La trampa de un solo worker

Con un único worker, una tarea que envía otra al mismo executor y llama a result() nunca libera el worker necesario para ejecutar la segunda.

Aumentar la cantidad solo oculta el problema estructural. Elimina el ciclo.

Backpressure

Enviar millones de tareas de una vez consume memoria para argumentos, futuros y queues internas. Produce trabajo en lotes o mantiene una ventana limitada.

def ejecutar_lotes(executor, items, tamano=100):
    lote = []
    for item in items:
        lote.append(executor.submit(procesar, item))
        if len(lote) == tamano:
            for futuro in lote:
                yield futuro.result()
            lote.clear()

Una ventana deslizante puede enviar un reemplazo cada vez que termina un futuro.

Inicialización de workers

Los executors pueden configurar cada worker mediante un initializer. Si falla, el pool puede quedar inutilizable y los futuros pendientes reciben un error de executor roto.

Mantén la inicialización corta, determinística y, cuando sea posible, independiente de servicios de red frágiles.

Estado global y procesos

Los procesos no comparten objetos Python normales. Cada worker tiene sus propios módulos y globals. Cambiar una variable global del worker no actualiza automáticamente al padre.

Pasa entradas de forma explícita y devuelve resultados. Para la capa de procesos del sistema, consulta posix en Python y usa abstracciones de multiprocessing.

Serialización

Funciones locales, lambdas, generators abiertos, locks, sockets y conexiones activas normalmente no pueden enviarse a un pool de procesos. Define funciones a nivel de módulo y pasa datos simples.

Los objetos grandes aumentan el coste de copia. Puede ser mejor que cada worker abra su recurso desde un identificador.

Callbacks

add_done_callback() registra una función ejecutada después de la finalización.

def al_terminar(futuro):
    try:
        futuro.result()
    except Exception:
        metricas.fallo()
    else:
        metricas.exito()

futuro.add_done_callback(al_terminar)

Los callbacks deben ser cortos y no bloquear. Publica un evento en una queue para trabajo mayor.

Contexto y logging

Las threads comparten logging, pero el contexto de una request puede no propagarse automáticamente. Los procesos necesitan configuración propia o logging basado en queue.

Incluye un identificador de tarea para conectar entrada, intento, duración, worker y excepción.

Retries

Los executors no repiten llamadas fallidas automáticamente. Reintenta solo fallos probablemente transitorios, con límite, demora, jitter e idempotencia.

Los errores de validación no mejoran al repetirse. Una escritura parcial puede duplicar datos sin clave idempotente.

Seguridad

No envíes funciones o payloads serializados proporcionados por usuarios no confiables a un pool de procesos. La serialización Python no es un formato seguro para entrada hostil.

Limita la concurrencia para no provocar denial of service contra tu propia base, filesystem, red o API externa.

Pruebas

Prueba éxito, excepción, timeout, cancelación antes del inicio, shutdown, pool roto, finalización desordenada, entrada vacía, tareas lentas e interrupción. Usa pocos workers para exponer dependencias ocultas.

Evita assertions basadas en timing exacto. Sincroniza con eventos y estado observable.

Errores comunes

Los fallos frecuentes son usar threads para CPU sin medir, crear procesos sin la protección del módulo principal, llamar result() desde tareas del mismo pool, enviar trabajo ilimitado, pensar que un timeout mata la ejecución, ignorar excepciones, pasar objetos no serializables y olvidar cerrar el executor.

Conclusión

concurrent.futures ofrece una capa uniforme orientada a tareas. Usa threads para I/O con espera, procesos para CPU cuando corresponda, limita workers, aplica backpressure y trata cada futuro como una operación que puede fallar, expirar o cancelarse.

Consulta la documentación oficial de concurrent.futures y signal en Python para shutdown coordinado.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Código Python asíncrono en un portátil para inspect.markcoroutinefunction
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    markcoroutinefunction: detecta wrappers async

    Aprende inspect.markcoroutinefunction en Python para identificar wrappers asíncronos, integrar frameworks y evitar detecciones incorrectas.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Código Python para recorrer carpetas y archivos con Path.walk
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk: recorre directorios con seguridad

    Aprende Path.walk en Python para recorrer directorios, filtrar archivos, tratar errores y controlar la travesía con seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    10/10/2026
    Depuración de un proceso Python en terminal con código
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pdb -p: depura procesos Python en ejecución

    Aprende a conectar pdb a un proceso Python en ejecución, inspeccionar la pila y diagnosticar bloqueos de forma segura.

    Ler mais

    Tempo de leitura: 6 minutos
    09/10/2026
    Código Python para representar fracciones exactas
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    fractions.from_number: convierte números en fracciones

    Aprende fractions.from_number en Python para convertir números en fracciones exactas, controlar precisión, validar entradas y evitar redondeos inesperados.

    Ler mais

    Tempo de leitura: 4 minutos
    09/10/2026
    Desarrollador configurando un servidor HTTPS y certificado TLS con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crea un servidor HTTPS local en Python

    Aprende HTTPSServer en Python para crear servicios HTTPS locales, configurar certificados, usar hilos y comprender sus límites.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Archivos protegidos que representan extracción segura de TAR con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extrae TAR con seguridad

    Aprende tarfile extraction_filter en Python para extraer archivos TAR con validación, seguridad y control de rutas.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026