Threading: O que É e Como Funciona | Python Brasil
Aprenda threading em Python: criar threads, sincronizacao, locks, ThreadPoolExecutor, GIL e boas praticas para programacao concorrente.
O que e Threading?
Threading e uma forma de concorrencia em Python que permite executar multiplas tarefas aparentemente ao mesmo tempo dentro de um unico processo. Cada thread e um fluxo de execucao independente que compartilha a memoria do processo pai. O modulo threading da biblioteca padrao fornece uma interface de alto nivel para criar e gerenciar threads.
E importante entender que, devido ao GIL (Global Interpreter Lock) do CPython, threads Python nao executam codigo Python simultaneamente em multiplos nucleos. Threads sao mais eficientes para tarefas I/O-bound (como requisicoes de rede, leitura de arquivos e consultas a banco de dados) do que para tarefas CPU-bound.
Criando Threads
import threading
import time
# Forma 1: Passando funcao ao Thread
def baixar_arquivo(url: str):
print(f'Baixando {url}...')
time.sleep(2) # simula download
print(f'Download completo: {url}')
# Criar e iniciar threads
t1 = threading.Thread(target=baixar_arquivo, args=('arquivo1.zip',))
t2 = threading.Thread(target=baixar_arquivo, args=('arquivo2.zip',))
t1.start()
t2.start()
# Aguardar ambas terminarem
t1.join()
t2.join()
print('Todos os downloads completos')
# Forma 2: Herdando de Thread
class MeuWorker(threading.Thread):
def __init__(self, nome: str, tarefas: list):
super().__init__()
self.nome = nome
self.tarefas = tarefas
self.resultados = []
def run(self):
for tarefa in self.tarefas:
print(f'{self.nome} processando: {tarefa}')
time.sleep(1)
self.resultados.append(f'{tarefa}_pronto')
worker = MeuWorker('Worker-1', ['A', 'B', 'C'])
worker.start()
worker.join()
print(worker.resultados)
ThreadPoolExecutor
O concurrent.futures oferece uma interface mais moderna e Pythonica para trabalhar com threads.
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def buscar_dados(url: str) -> dict:
"""Simula uma requisicao HTTP."""
print(f'Buscando {url}...')
time.sleep(2)
return {'url': url, 'status': 200, 'dados': f'conteudo de {url}'}
urls = [
'https://api.exemplo.com/usuarios',
'https://api.exemplo.com/produtos',
'https://api.exemplo.com/pedidos',
'https://api.exemplo.com/relatorios',
]
# Executar em paralelo com pool de threads
with ThreadPoolExecutor(max_workers=4) as executor:
# submit retorna Future objects
futures = {executor.submit(buscar_dados, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
resultado = future.result()
print(f'Concluido: {resultado["url"]} - Status: {resultado["status"]}')
except Exception as e:
print(f'Erro em {url}: {e}')
# Alternativa com map (preserva a ordem)
with ThreadPoolExecutor(max_workers=4) as executor:
resultados = list(executor.map(buscar_dados, urls))
for r in resultados:
print(r['url'])
Sincronizacao com Lock
Quando multiplas threads acessam dados compartilhados, e necessario sincronizar o acesso para evitar condicoes de corrida.
import threading
# Sem lock — problematico
contador_inseguro = 0
def incrementar_inseguro():
global contador_inseguro
for _ in range(100000):
contador_inseguro += 1 # nao e atomico!
# Com lock — seguro
contador = 0
lock = threading.Lock()
def incrementar_seguro():
global contador
for _ in range(100000):
with lock: # adquire e libera automaticamente
contador += 1
# Testando
threads = [threading.Thread(target=incrementar_seguro) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f'Contador: {contador}') # Sempre 500000
# RLock — permite re-entrada pela mesma thread
rlock = threading.RLock()
def funcao_recursiva(n):
with rlock:
if n > 0:
funcao_recursiva(n - 1)
Outras Primitivas de Sincronizacao
import threading
import time
# Event — sinalizar entre threads
evento = threading.Event()
def produtor():
print('Preparando dados...')
time.sleep(3)
print('Dados prontos!')
evento.set() # sinaliza que esta pronto
def consumidor():
print('Aguardando dados...')
evento.wait() # bloqueia ate o evento ser setado
print('Processando dados recebidos')
# Semaphore — limitar acesso concorrente
semaforo = threading.Semaphore(3) # maximo 3 threads simultaneas
def acessar_recurso(thread_id: int):
with semaforo:
print(f'Thread {thread_id} acessando recurso')
time.sleep(2)
print(f'Thread {thread_id} liberou recurso')
# Barrier — sincronizar N threads em um ponto
barreira = threading.Barrier(3)
def fase_processamento(thread_id: int):
print(f'Thread {thread_id}: fase 1 completa')
barreira.wait() # todas esperam aqui
print(f'Thread {thread_id}: iniciando fase 2')
Thread-Safe Queue
import threading
import queue
import time
# Padrao produtor-consumidor
fila = queue.Queue(maxsize=10)
def produtor(fila: queue.Queue, itens: list):
for item in itens:
fila.put(item)
print(f'Produzido: {item}')
time.sleep(0.5)
fila.put(None) # sentinela para encerrar
def consumidor(fila: queue.Queue):
while True:
item = fila.get()
if item is None:
break
print(f'Consumido: {item}')
fila.task_done()
# Executar
t_prod = threading.Thread(target=produtor, args=(fila, ['A', 'B', 'C', 'D']))
t_cons = threading.Thread(target=consumidor, args=(fila,))
t_prod.start()
t_cons.start()
t_prod.join()
t_cons.join()
O GIL (Global Interpreter Lock)
import threading
import time
# Tarefa CPU-bound — threads NAO ajudam por causa do GIL
def calcular_pesado(n):
total = sum(i * i for i in range(n))
return total
# Com threads — nao melhora (pode ate piorar)
inicio = time.time()
threads = [threading.Thread(target=calcular_pesado, args=(10_000_000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
tempo_threads = time.time() - inicio
print(f'Com threads: {tempo_threads:.2f}s')
# Para CPU-bound, use multiprocessing em vez de threading
Erros Comuns
O erro mais perigoso e criar condicoes de corrida (race conditions) ao acessar dados compartilhados sem sincronizacao. Outro erro frequente e criar deadlocks — quando duas threads esperam uma pela outra para liberar recursos. Esquecer de chamar join() pode fazer o programa terminar antes das threads concluirem. Usar threads para tarefas CPU-bound e ineficaz por causa do GIL. Tambem e comum criar threads demais, consumindo memoria e causando overhead de troca de contexto.
Boas Praticas
Prefira ThreadPoolExecutor em vez de gerenciar threads manualmente. Use queue.Queue para comunicacao entre threads. Sempre use context managers (with lock:) para sincronizacao. Limite o numero de threads ao necessario. Para tarefas CPU-bound, use multiprocessing. Para I/O assincrono moderno, considere asyncio. Evite compartilhar estado mutavel entre threads sempre que possivel.
Quando Usar
Threading e ideal para tarefas I/O-bound como requisicoes HTTP paralelas, leitura de multiplos arquivos, consultas concorrentes a bancos de dados e qualquer operacao que passe a maior parte do tempo aguardando respostas externas. Para CPU-bound, use multiprocessing; para I/O-bound com muitas conexoes simultaneas, considere asyncio.