Diferenças entre Threading e ThreadPoolExecutor
Entendendo o Conceito
O Python disponibiliza duas formas principais para lidar com execução concorrente baseada em threads: o módulo threading (baixo nível) e a classe ThreadPoolExecutor do módulo concurrent.futures (alto nível).
O Threading direto, utilizando a classe threading.Thread, representa a criação individual de uma linha de execução de forma explícita. Cada thread criada gera uma chamada diretamente ao sistema operacional, o que exige alocação individual de recursos de sistema.
O ThreadPoolExecutor gerencia um grupo (pool) de threads de trabalho pré-criadas e reutilizáveis. Em vez de criar e destruir threads a cada tarefa, o executor mantém as threads ativas em uma fila, distribuindo as tarefas conforme a disponibilidade delas.
Ambas as abordagens compartilham o mesmo espaço de memória do processo e são limitadas pelo Global Interpreter Lock (GIL) do Python. Ambas são indicadas para tarefas que envolvem operações de entrada e saída (I/O-bound), como requisições de rede ou leitura de arquivos, onde o tempo de espera predomina sobre a computação ativa.
Passo a Passo
Esta seção apresenta exemplos práticos comparando a criação direta de threads com a abordagem utilizando um pool de threads para a simulação de requisições de rede.
Abordagem 1: Uso Direto de Threading
O uso direto de threading.Thread é indicado para cenários com poucas tarefas ou quando é necessário controle total sobre o ciclo de vida individual de cada linha de execução.
Definição das tarefas e estrutura básica:
import threading
import time
def realizar_requisicao(url):
print(f"Iniciando requisicao para {url}")
time.sleep(2)
print(f"Requisicao para {url} concluida")
urls = ["https://api.exemplo.com/dados", "https://api.exemplo.com/usuarios", "https://api.exemplo.com/produtos"]
threads = []
Criação e inicialização das threads individuais:
for url in urls:
t = threading.Thread(target=realizar_requisicao, args=(url,))
threads.append(t)
t.start()
Sincronização e aguardo do término de todas as execuções:
for t in threads:
t.join()
Abordagem 2: Uso de ThreadPoolExecutor
O ThreadPoolExecutor é recomendado para processar listas volumosas de tarefas semelhantes de forma controlada, limitando o número de threads simultâneas para poupar recursos do sistema.
Definição das tarefas e dados:
from concurrent.futures import ThreadPoolExecutor
import time
def processar_download(arquivo):
print(f"Baixando {arquivo}")
time.sleep(1.5)
return f"Conteudo de {arquivo}"
arquivos = ["foto.png", "relatorio.pdf", "dados.csv", "video.mp4"]
Inicialização do executor com controle de concorrência limitada:
with ThreadPoolExecutor(max_workers=2) as executor:
resultados = executor.map(processar_download, arquivos)
Consumo e processamento dos resultados gerados de maneira síncrona após a finalização:
for resultado in resultados:
print(resultado)
Explicando o código
threading.Thread(target=realizar_requisicao, args=(url,)):target: Especifica a função que será executada na nova thread de forma assíncrona.args: Passa os parâmetros posicionais necessários para a execução da função de destino.
t.start():- Dispara a execução paralela do método configurado na thread.
t.join():- Bloqueia a execução do fluxo principal até que a thread específica finalize seu ciclo de vida.
ThreadPoolExecutor(max_workers=2):max_workers: Define o limite máximo de threads ativas simultaneamente no pool de trabalho. Se não configurado, assume um padrão baseado no número de núcleos de CPU disponíveis.
executor.map(processar_download, arquivos):- Aplica a função de maneira assíncrona sobre cada item do iterável fornecido, funcionando de forma semelhante ao
mapnativo do Python.
- Aplica a função de maneira assíncrona sobre cada item do iterável fornecido, funcionando de forma semelhante ao
- Bloco
with(Context Manager):- Garante a liberação automática dos recursos ao sair do bloco, acionando o encerramento do pool e aguardando as tarefas pendentes.
Observações
- Sobrecarga de Criação: A criação repetida de novas instâncias de
threading.Threadem loops longos causa consumo excessivo de memória e processamento devido ao custo de inicialização de threads no sistema operacional. OThreadPoolExecutorcontorna este problema ao reutilizar threads existentes. - Retorno de Valores: Coletar o retorno de funções executadas diretamente via
threading.Threadexige a implementação manual de estruturas de fila (queue.Queue), enquanto oThreadPoolExecutorentrega os resultados ou objetosFuturede forma nativa. - Tratamento de Exceções: Erros disparados dentro de threads criadas de forma direta podem passar despercebidos ou encerrar a thread sem tratamento centralizado. No executor, as exceções são associadas ao objeto
Futuree são propagadas quando o resultado correspondente é requisitado pelo fluxo principal. - Limitação do GIL: Nenhuma das abordagens contorna o bloqueio global do interpretador (GIL) para tarefas computacionais intensivas no CPython padrão, sendo ambas ineficientes para tarefas que demandam uso massivo de processamento de CPU.
Referências
- Documentação oficial do módulo
threadingem Python:https://docs.python.org/3/library/threading.html - Documentação oficial do módulo
concurrent.futuresem Python:https://docs.python.org/3/library/concurrent.futures.html - Comparativo técnico de performance entre Thread e ThreadPoolExecutor:
https://superfastpython.com/threadpoolexecutor-vs-threads/ - Discussões sobre casos de uso e concorrência no StackOverflow:
https://stackoverflow.com/questions/12547381/multithreading-vs-threadpoolexecutor