Threads e Automação em Paralelo em Python

Quando falamos em desempenho e eficiência, um dos temas mais importantes na programação é a execução em paralelo. Muitas vezes, temos tarefas que podem ser realizadas ao mesmo tempo (como baixar vários arquivos, processar imagens ou consultar múltiplas APIs). É aí que entram as threads em Python.

Neste artigo, você vai aprender:

  • O que são threads.

  • Diferença entre paralelismo e concorrência.

  • Como usar o módulo threading.

  • Exemplo prático de automação em paralelo.

  • Cuidados e boas práticas no uso de threads.


🔹 1. O que são Threads?

Uma thread é uma unidade de execução dentro de um processo.

  • Um processo é um programa em execução (como o Python rodando um script).

  • Cada processo pode ter múltiplas threads, que compartilham memória e recursos.

Threads permitem que o programa faça várias coisas ao mesmo tempo, como:
✅ Baixar arquivos em paralelo.
✅ Ler dados enquanto processa outros.
✅ Realizar cálculos sem travar a interface gráfica.


🔹 2. Concorrência vs Paralelismo

  • Concorrência: quando várias tarefas parecem rodar ao mesmo tempo, mas na prática são alternadas rapidamente pelo sistema.

  • Paralelismo: quando múltiplas tarefas são realmente executadas ao mesmo tempo, em núcleos diferentes do processador.

👉 Em Python, devido ao GIL (Global Interpreter Lock), as threads não executam código Python puro em paralelo nos múltiplos núcleos. Mas elas são extremamente úteis para tarefas I/O-bound (entrada e saída), como:

  • Requisições de rede.

  • Leitura e escrita em arquivos.

  • Acesso a bancos de dados.


🔹 3. Usando o módulo threading

Python fornece o módulo threading, que facilita a criação e controle de threads.

Criando uma thread simples

import threading
import time

def tarefa(nome):
    for i in range(3):
        print(f"Executando {nome} - passo {i+1}")
        time.sleep(1)

# Criando duas threads
t1 = threading.Thread(target=tarefa, args=("Thread 1",))
t2 = threading.Thread(target=tarefa, args=("Thread 2",))

# Iniciando as threads
t1.start()
t2.start()

# Esperando as threads terminarem
t1.join()
t2.join()

print("Execução finalizada!")

🔎 Saída esperada (ordem pode variar):

Executando Thread 1 - passo 1
Executando Thread 2 - passo 1
Executando Thread 1 - passo 2
Executando Thread 2 - passo 2
...
Execução finalizada!

🔹 4. Exemplo prático – Download de múltiplas páginas em paralelo

Imagine que você queira baixar o conteúdo de várias páginas da web ao mesmo tempo.

import threading
import requests
import time

urls = [
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/3",
    "https://httpbin.org/delay/1"
]

def baixar(url):
    print(f"Iniciando download: {url}")
    resposta = requests.get(url)
    print(f"Concluído: {url} - Status {resposta.status_code}")

threads = []

inicio = time.time()

# Criando uma thread para cada download
for url in urls:
    t = threading.Thread(target=baixar, args=(url,))
    threads.append(t)
    t.start()

# Aguardando todas terminarem
for t in threads:
    t.join()

fim = time.time()
print(f"Tempo total: {fim - inicio:.2f} segundos")

✅ Sem threads, cada download seria feito em sequência, levando 6 segundos ou mais.
✅ Com threads, as requisições são feitas em paralelo, reduzindo o tempo para aproximadamente 3 segundos.


🔹 5. Usando ThreadPoolExecutor (mais simples e moderno)

Em vez de gerenciar threads manualmente, podemos usar o concurrent.futures:

from concurrent.futures import ThreadPoolExecutor
import requests

urls = [
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/3",
    "https://httpbin.org/delay/1"
]

def baixar(url):
    print(f"Baixando {url}")
    resposta = requests.get(url)
    return f"{url} -> {resposta.status_code}"

with ThreadPoolExecutor(max_workers=3) as executor:
    resultados = executor.map(baixar, urls)

for r in resultados:
    print(r)

👉 O ThreadPoolExecutor facilita o uso de múltiplas threads, controlando o número de workers automaticamente.


🔹 6. Cuidados com Threads

  • ⚠️ Compartilhamento de dados: como as threads compartilham memória, podem ocorrer condições de corrida.

  • ✅ Use threading.Lock() para proteger recursos críticos.

Exemplo:

import threading

contador = 0
lock = threading.Lock()

def incrementar():
    global contador
    for _ in range(100000):
        with lock:
            contador += 1

t1 = threading.Thread(target=incrementar)
t2 = threading.Thread(target=incrementar)

t1.start()
t2.start()
t1.join()
t2.join()

print("Contador:", contador)

Sem o lock, o valor final poderia ser incorreto.


🔹 7. Quando usar Threads?

✅ Útil para tarefas I/O-bound (rede, arquivos, banco de dados).
❌ Não é indicado para tarefas CPU-bound (cálculos pesados, criptografia, compressão) → nesse caso, use multiprocessing.


🔹 8. Conclusão

As threads em Python são uma ótima ferramenta para automação em paralelo quando o gargalo está em operações de entrada e saída. Elas:

  • Melhoram a performance de tarefas I/O-bound.

  • Permitem que várias operações rodem sem bloquear o programa.

  • São simples de implementar com threading e ainda mais fáceis com ThreadPoolExecutor.

Se você precisa de paralelismo real em cálculos pesados, considere usar multiprocessing.

Comentários

Postagens mais visitadas deste blog

Manipulação de Arquivos no C#: Como Ler, Escrever e Trabalhar com Arquivos de Forma Simples

Gerando Relatórios em PDF com Python (ReportLab e FPDF)

Laços de Repetição em Python: Conceitos e Exemplos Práticos