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.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: ThreadPoolExecutor na prática.
Os objetivos desta aula. 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.
Veja o essencial, parte por parte.
Por que usar um pool. O ThreadPoolExecutor gerencia um grupo de threads e distribui as tarefas por você.
submit, map e a coleta de resultados. Há duas formas principais de enviar trabalho ao pool.
Threads ou processos: a escolha final. Para I/O, você pode usar mais threads do que núcleos, pois a maioria fica esperando. Comece com um valor moderado e ajuste medindo.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
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ção | Ferramenta | Motivo |
|---|---|---|
| Baixar muitas páginas | ThreadPoolExecutor | I/O bound: threads liberam o GIL na espera |
| Ler e transformar muitos arquivos | ThreadPoolExecutor | Predomina espera de disco |
| Cálculo numérico pesado em lote | ProcessPoolExecutor | CPU bound: processos rodam em paralelo real |
| Compressão ou processamento de imagem | ProcessPoolExecutor | Cá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.