Wilberhg's blog

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

Observações

Referências

  1. Documentação oficial do módulo threading em Python: https://docs.python.org/3/library/threading.html
  2. Documentação oficial do módulo concurrent.futures em Python: https://docs.python.org/3/library/concurrent.futures.html
  3. Comparativo técnico de performance entre Thread e ThreadPoolExecutor: https://superfastpython.com/threadpoolexecutor-vs-threads/
  4. Discussões sobre casos de uso e concorrência no StackOverflow: https://stackoverflow.com/questions/12547381/multithreading-vs-threadpoolexecutor

#automation #concurrency #multithreading #python #threadpoolexecutor