Módulo 8 - Concorrência com threads

ThreadPoolExecutor na prática

11 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Entender o que é um pool de threads e por que reutilizá-las.
  • Usar ThreadPoolExecutor com submit e com map.
  • Coletar resultados com as_completed e tratar exceções.
  • Escolher entre threads e processos conforme a tarefa.

Por que usar um pool

Criar uma thread por tarefa tem dois problemas quando as tarefas são muitas. Primeiro, cada thread custa memória e tempo para criar; disparar mil threads de uma vez é desperdício e pode até sobrecarregar o sistema. Segundo, você acaba escrevendo o mesmo código de disparar, guardar em lista e dar join sempre. O ThreadPoolExecutor resolve os dois. Ele mantém um número fixo de threads prontas, um pool, e vai entregando as tarefas para as threads livres. Quando uma termina, ela pega a próxima da fila. Você define quantas threads quer no máximo e o pool cuida do resto, reaproveitando as mesmas threads em vez de criar e destruir sem parar.

O executor vive no módulo concurrent.futures, da biblioteca padrão. O jeito idiomático de usá-lo é com o with, que abre o pool ao entrar no bloco e, ao sair, espera todas as tarefas terminarem antes de fechar as threads. Isso elimina o join manual: quando o bloco with acaba, você tem a garantia de que tudo rodou. É o mesmo espírito dos context managers que você já usa para arquivos, agora aplicado a um grupo de threads.

submit, map e a coleta de resultados

Há duas formas principais de enviar trabalho ao pool. A primeira é o submit, que envia uma tarefa e devolve na hora um future, o objeto que representará o resultado quando ficar pronto. Você guarda os futures e depois pede o result de cada um. A segunda é o map, mais direto quando você quer aplicar a mesma função a uma coleção de itens: ele devolve os resultados na ordem dos itens de entrada, como o map embutido, só que rodando em paralelo lógico. Use o map quando a operação é uniforme sobre uma lista; use o submit quando as tarefas são variadas ou você quer tratar cada resultado assim que ele chega.

from concurrent.futures import ThreadPoolExecutor
import time

def baixar(url):
    time.sleep(1)          # simula espera de rede
    return f"conteudo de {url}"

urls = [f"site-{i}" for i in range(6)]

# com map: resultados na ordem das urls
with ThreadPoolExecutor(max_workers=4) as executor:
    resultados = list(executor.map(baixar, urls))

for r in resultados:
    print(r)

executor.map aplica a função a cada url em paralelo lógico e devolve na ordem de entrada.

Quando você quer reagir aos resultados na ordem em que ficam prontos, e não na ordem de envio, use as_completed. Ela recebe os futures e vai devolvendo cada um assim que termina, o que é ótimo para mostrar progresso ou tratar o mais rápido primeiro. Um cuidado importante: se a tarefa lançar uma exceção, ela fica guardada no future e só estoura quando você chama result. Por isso, envolva o result em try e except para não perder erros silenciosamente.

from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def tarefa(n):
    time.sleep(n * 0.2)
    if n == 3:
        raise ValueError("falhou na 3")
    return n * n

with ThreadPoolExecutor(max_workers=3) as executor:
    futuros = {executor.submit(tarefa, n): n for n in range(5)}
    for futuro in as_completed(futuros):
        n = futuros[futuro]
        try:
            print(f"{n} -> {futuro.result()}")
        except ValueError as e:
            print(f"{n} deu erro: {e}")

submit devolve futures; as_completed entrega quem termina antes; result relança a exceção da tarefa.

Threads ou processos: a escolha final

O mesmo módulo concurrent.futures oferece o ProcessPoolExecutor, gêmeo do ThreadPoolExecutor mas com processos no lugar de threads. A escolha entre eles fecha o raciocínio do módulo inteiro. Se a tarefa é I/O bound, esperar rede, disco ou banco, use o ThreadPoolExecutor: as threads liberam o GIL durante a espera e o ganho é grande, com pouco custo. Se a tarefa é CPU bound, cálculo puro, use o ProcessPoolExecutor: cada processo tem seu próprio interpretador, sem GIL compartilhado, então roda em paralelo real nos vários núcleos. A interface é quase idêntica, o que torna a troca simples depois que você mediu onde está o gargalo.

SituaçãoFerramentaMotivo
Baixar muitas páginasThreadPoolExecutorI/O bound: threads liberam o GIL na espera
Ler e transformar muitos arquivosThreadPoolExecutorPredomina espera de disco
Cálculo numérico pesado em loteProcessPoolExecutorCPU bound: processos rodam em paralelo real
Compressão ou processamento de imagemProcessPoolExecutorCálculo intenso, GIL atrapalharia threads

Mesma interface, decisão diferente: I/O pede threads; CPU pede processos.

Teste rápido

Você precisa processar milhares de números com cálculo pesado. Qual executor usar?

Perguntas frequentes

Qual a vantagem do ThreadPoolExecutor sobre criar threads na mão?
Ele reaproveita um número fixo de threads em vez de criar uma por tarefa, evita o código repetitivo de start e join, entrega os resultados prontos via futures e fecha tudo sozinho quando o bloco with termina. Para trabalho em lote, é mais limpo, seguro e fácil de manter.
Qual a diferença entre submit e map?
O submit envia uma tarefa e devolve um future, dando controle fino sobre cada resultado, inclusive tratar exceções e usar as_completed. O map aplica uma função a uma coleção e devolve os resultados na ordem de entrada, mais direto quando a operação é uniforme sobre uma lista.
Como trato erros de uma tarefa no pool?
Uma exceção lançada dentro da tarefa fica guardada no future e só é relançada quando você chama result. Por isso, envolva a chamada a result em try e except. Se usar map, a exceção estoura ao consumir o resultado correspondente durante a iteração.
Quantas threads devo colocar no max_workers?
Depende da tarefa. Para I/O, você pode usar bem mais threads do que núcleos, porque a maioria fica esperando; comece com um valor moderado e ajuste medindo. Não existe número mágico: mais threads nem sempre é mais rápido, e um pool grande demais pode sobrecarregar o serviço chamado.
Quando trocar o ThreadPoolExecutor pelo ProcessPoolExecutor?
Troque quando a tarefa for CPU bound, ou seja, cálculo puro que segura o GIL. Os processos rodam em paralelo real nos vários núcleos, sem GIL compartilhado. A interface é quase igual, então a mudança costuma ser pequena depois que o profiling confirma que o gargalo é a conta, não a espera.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.