concurrent.futures oferece uma API comum para enviar funções a pools de threads ou processos. Threads costumam servir para tarefas que esperam rede ou disco; processos podem ajudar em trabalho de CPU que seja serializável e suficientemente grande.

Coletar resultados conforme terminam

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


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


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

Defina timeouts também na operação executada. Um timeout ao esperar pelo Future não interrompe automaticamente a chamada de rede. Limite entrada, tamanho de resposta e quantidade de workers para não transformar concorrência em pressão sobre memória ou serviços externos.

Evite uma tarefa do pool esperando por outra tarefa do mesmo pool quando não há workers livres; esse padrão pode causar deadlock. Com ProcessPoolExecutor, funções e argumentos precisam ser serializáveis, e o ponto de entrada deve ser protegido em plataformas que iniciam novos processos importando o módulo.

Para milhares de conexões cooperativas, async e await em Python podem ser mais adequados. A documentação oficial de concurrent.futures, consultada em 22 de julho de 2026, detalha executores, futures, cancelamento e riscos de deadlock.

Escolher o executor pelo trabalho

ThreadPoolExecutor atende funções que passam boa parte do tempo esperando rede, arquivos ou banco. Threads compartilham memória, então estado mutável exige sincronização. Código Python pesado de CPU geralmente não ganha paralelismo em threads no CPython tradicional. ProcessPoolExecutor usa intérpretes separados, pagando inicialização e serialização. Meça o lote completo: processos podem ser mais lentos em tarefas pequenas, e algumas bibliotecas nativas liberam o GIL.

Ordem, resultados e exceções

map() entrega resultados na ordem de entrada. submit() com as_completed() permite consumir por conclusão e manter contexto:

itens = [" Alpha ", "Beta", " gama"]
with ThreadPoolExecutor(max_workers=3) as executor:
    pendentes = {executor.submit(str.strip, item): item for item in itens}
    for future in as_completed(pendentes):
        original = pendentes[future]
        try:
            print(original, future.result())
        except Exception as exc:
            print(f"falha em {original!r}: {exc}")

result() relança a exceção do worker. Observe todo future para não esconder falha parcial. Prefira retornar um resultado imutável a imprimir ou alterar globais. O coordenador decide o que persistir e como relatar sucesso parcial. Se "não encontrado" é resultado esperado, modele esse estado; não transforme toda ocorrência em exceção.

Limitar submissão e pressão

Poucos workers não limitam memória quando milhões de futures são criados imediatamente. Alimente o executor em janelas, use fila limitada ou buffering disponível na versão suportada. Dimensione o pool pela dependência: cinquenta threads não ajudam um banco que aceita dez conexões.

Timeout ao esperar não mata o código em execução. Chamadas de rede e banco precisam dos próprios timeouts. Tarefas cooperativas podem consultar threading.Event em pontos seguros. cancel() só funciona antes do início, e não há forma segura de matar uma thread arbitrária.

O context manager espera o trabalho submetido. Em falhas, shutdown(cancel_futures=True) cancela o que ainda está na fila e deixa tarefas iniciadas terminarem. Mantenha operações curtas e idempotentes quando possível.

Processos com segurança

Proteja a criação do pool com if __name__ == "__main__":, principalmente em plataformas que iniciam processos importando o módulo. Envie funções de nível superior e valores serializáveis. Lambdas, funções internas, locks, arquivos abertos e clientes ativos costumam falhar.

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()

Evite reenviar grandes dados imutáveis. Inicialize estado local quando apropriado e agrupe tarefas pequenas, sem criar blocos tão grandes que prejudiquem balanceamento.

Deadlocks, recursos e transações

Um worker não deve aguardar outro future enfileirado no mesmo pool saturado. Mantenha dependências no coordenador. Pools diferentes podem isolar uma dependência lenta, mas cada um precisa de limite.

Não passe conexão de banco, cliente HTTP ou transação a um processo. Em threads, confirme que o cliente é thread-safe. A transação deve normalmente ficar no chamador, que valida todos os resultados antes do commit. Isso evita metade do lote persistida após falha.

Pool controla simultaneidade, não requisições por segundo. Rate limits exigem um limitador. Retries também ocupam workers, portanto use poucas tentativas e backoff.

Observar e testar

Meça espera na fila separadamente da execução. Registre tarefas submetidas, concluídas, falhas e canceladas, sem labels de alta cardinalidade. Passe identificadores de correlação como valores serializáveis.

Teste exceção, timeout, conclusão parcial, cancelamento e shutdown. Não dependa da ordem exata, salvo quando ela é o contrato. Use serviços locais controlados. Executores não oferecem filas duráveis nem execução distribuída; para trabalho que precisa sobreviver a reinícios, use um sistema de jobs. Para muitas APIs assíncronas, considere asyncio.

Comece com código sequencial correto, faça profiling e introduza concorrência limitada. Propriedade, capacidade, política de erro e encerramento devem permanecer explícitos.

Tamanho de lotes e justiça

Agrupar entradas reduz o custo de serialização no pool de processos, mas blocos grandes demais pioram o balanceamento. Um worker pode receber itens caros enquanto os demais ficam ociosos. Faça benchmark com dados representativos e distribuição irregular, não apenas com entradas homogêneas.

Também vale separar dependências com perfis diferentes. Uma API lenta não deveria ocupar todas as threads necessárias a uma API rápida. Pools distintos criam isolamento, desde que o orçamento total de threads, conexões e memória continue limitado.

Inicialização e encerramento de recursos

Workers de processo podem criar estado local por meio de um inicializador suportado pelo executor. Use isso para dados somente leitura ou clientes que precisam nascer dentro do processo. Não dependa de conexões abertas antes do fork. Documente como os recursos são fechados e como uma falha de inicialização afeta o pool.

Em servidores, o executor deve ter um dono com ciclo de vida definido. Criá-lo dentro de cada requisição adiciona custo e pode multiplicar threads sem controle. Um pool compartilhado precisa de fila limitada e proteção contra uma única rota que consuma toda a capacidade.

Prioridade e durabilidade

O executor básico não oferece prioridade. Para trabalho urgente e tarefas de manutenção, use capacidades separadas ou um agendador apropriado. Tampouco existe persistência: se o processo termina, futures pendentes desaparecem. Não trate um future como confirmação durável de que um email, cobrança ou exportação será executado.

Quando a entrega precisa sobreviver a falhas, registre a intenção de forma transacional e encaminhe a um sistema de filas. O executor ainda pode ser útil dentro do worker dessa fila para paralelismo local limitado.

Revisão de produção

Antes de liberar, confirme número máximo de workers, limite de entradas pendentes, timeouts internos, política de retry e comportamento de shutdown. Simule uma tarefa travada e uma exceção em meio ao lote. Verifique que logs identificam o item sem dados sensíveis e que métricas revelam saturação.

O ganho deve aparecer em uma medição real de throughput ou latência. Se a complexidade cresce sem benefício claro, a versão sequencial continua sendo a escolha mais fácil de operar.