Multithreading em Python: Além do Básico

multithreading é uma técnica que permite que múltiplas threads executem simultaneamente dentro de um mesmo processo. Em Python, ele é amplamente usado para tarefas de I/O, redes e paralelização leve, embora existam limitações devido ao Global Interpreter Lock (GIL).


1. Conceitos Fundamentais

1.1 Threads vs Processos

CaracterísticaThreadProcesso
MemóriaCompartilhadaIndependente
CriaçãoLevePesada
GILAfeta CPU-boundNão afeta CPU-bound
UsoI/O-boundCPU-bound
  • Threads são mais eficientes para tarefas de I/O, mas limitadas para computação pura em Python devido ao GIL.

  • Para tarefas CPU-bound, prefira multiprocessing.


1.2 Criando Threads

import threading

def tarefa(nome):
    print(f"Tarefa {nome} iniciada")

threads = []
for i in range(5):
    t = threading.Thread(target=tarefa, args=(i,))
    t.start()
    threads.append(t)

for t in threads:
    t.join()
  • join() garante que o programa espere todas as threads terminarem.


2. Threads com Classes

  • Usar classes herdando de threading.Thread permite maior controle.

class MinhaThread(threading.Thread):
    def __init__(self, nome):
        super().__init__()
        self.nome = nome

    def run(self):
        print(f"Tarefa {self.nome} executando")

threads = [MinhaThread(i) for i in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()
  • run() é o método que executa quando start() é chamado.


3. Sincronização de Threads

Quando múltiplas threads compartilham dados, condições de corrida podem ocorrer.

3.1 Locks (Travas)

lock = threading.Lock()
contador = 0

def tarefa():
    global contador
    for _ in range(1000):
        with lock:  # protege a seção crítica
            contador += 1

threads = [threading.Thread(target=tarefa) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(contador)  # sempre 5000
  • Sem o lock, contador poderia ficar inconsistente.

3.2 RLocks (Reentrant Locks)

  • Permite uma mesma thread adquirir o lock múltiplas vezes sem deadlock.

rlock = threading.RLock()

4. Thread-safe Queues

import queue
import time

fila = queue.Queue()

def produtor():
    for i in range(5):
        fila.put(i)
        print(f"Produziu {i}")
        time.sleep(0.1)

def consumidor():
    while True:
        item = fila.get()
        print(f"Consumiu {item}")
        fila.task_done()
        if item == 4: break

t1 = threading.Thread(target=produtor)
t2 = threading.Thread(target=consumidor)

t1.start()
t2.start()
t1.join()
t2.join()
  • Garante sincronização automática entre threads.


5. ThreadPoolExecutor

  • Gerencia um pool de threads, simplificando criação e controle.

from concurrent.futures import ThreadPoolExecutor
import time

def tarefa(x):
    time.sleep(1)
    return x**2

with ThreadPoolExecutor(max_workers=5) as executor:
    resultados = list(executor.map(tarefa, range(10)))

print(resultados)
  • Automatiza start, join e gerenciamento de threads.

  • Útil para I/O-bound ou múltiplas requisições de rede.


6. Estratégias Avançadas

6.1 Evitando GIL para CPU-bound

  • Python threads não aceleram processamento puro em CPU-bound.

  • Estratégias:

    • Usar multiprocessing em vez de threads

    • Combinar threads para I/O e processos para CPU

    • Usar bibliotecas que liberam GIL (NumPyCythonNumba)

6.2 Deadlocks

  • Evite adquirir múltiplos locks sem ordem consistente.

  • Use RLocks ou timeout no lock.

if lock.acquire(timeout=2):
    try:
        # código protegido
    finally:
        lock.release()

6.3 Comunicação entre threads

evento = threading.Event()

def worker():
    print("Esperando sinal...")
    evento.wait()
    print("Sinal recebido!")

t = threading.Thread(target=worker)
t.start()

# Sinaliza após 2s
time.sleep(2)
evento.set()

7. Boas Práticas Profissionais

  1. Prefira ThreadPoolExecutor para simplificar código.

  2. Proteja sempre dados compartilhados com locks ou queues.

  3. Evite threads para cálculos intensivos, use multiprocessing.

  4. Evite deadlocks e garanta ordem consistente de aquisição de locks.

  5. Combine threads com asyncio para I/O moderno.

  6. Monitore threads ativas e logs para detecção de travamentos ou lentidão.


8. Aplicações Profissionais

  • Web scraping paralelo

  • Chamada concorrente de APIs

  • Processamento de arquivos e logs I/O-bound

  • Sistemas de fila produtor-consumidor

  • Monitoramento e coleta de dados em tempo real


9. Conclusão

  • Multithreading em Python é ótimo para I/O-bound, mas limitado em CPU-bound devido ao GIL.

  • Ferramentas como ThreadPoolExecutor, Locks, RLocks e Queues permitem código seguro e eficiente.

  • Combinando threads com paralelização de processos ou bibliotecas que liberam o GIL, é possível criar aplicações altamente performáticas e escaláveis.

Comentários

Postagens mais visitadas deste blog

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

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

Como Instalar o Xamarin com C#: Passo a Passo Completo