Multithreading em Python: Além do Básico
1. Conceitos Fundamentais
1.1 Threads vs Processos
Característica | Thread | Processo |
---|---|---|
Memória | Compartilhada | Independente |
Criação | Leve | Pesada |
GIL | Afeta CPU-bound | Não afeta CPU-bound |
Uso | I/O-bound | CPU-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 quandostart()
é 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
queue.Queue
é thread-safe e ideal para produtor-consumidor.
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 threadsCombinar threads para I/O e processos para CPU
Usar bibliotecas que liberam GIL (
NumPy
,Cython
,Numba
)
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
Use queues, eventos (
threading.Event
) e semáforos (threading.Semaphore
).
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
Prefira ThreadPoolExecutor para simplificar código.
Proteja sempre dados compartilhados com locks ou queues.
Evite threads para cálculos intensivos, use
multiprocessing
.Evite deadlocks e garanta ordem consistente de aquisição de locks.
Combine threads com asyncio para I/O moderno.
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
Postar um comentário