Voltar ao Glossario
Glossario Python

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.