Multithreading permite múltiplas threads dentro de um único processo. Cada thread executa um fluxo independente de instruções. Primeiramente, threads compartilham a mesma memória do processo. Por exemplo, duas threads podem acessar a mesma variável global. Isso facilita a comunicação entre elas. Além disso, threads são mais leves que processos completos. A voz passiva é usada aqui: “as threads são gerenciadas pelo sistema operacional”. Quando utilizar multithreading em Python? Em operações de I/O. Por exemplo, downloads de rede, leitura de arquivos ou consultas a banco. No entanto, o GIL (Global Interpreter Lock) limita a execução paralela. Portanto, threads não aceleram código com CPU intensivo. Vamos explorar criação, sincronização e boas práticas. Três subtítulos guiarão você pelo mundo do threading. Ao final, você dominará a concorrência em Python.
Criando e gerenciando threads
Python oferece o módulo threading para trabalhar com threads.
A classe Thread representa uma thread executável.
Para criar uma thread, instancie Thread(target=funcao).
Depois, chame start() para iniciar a execução.
O método join() espera a thread terminar.
Quando usar criação manual? Em scripts com poucas threads controladas.
A voz passiva é aplicada: “os argumentos são passados via args“.
Exemplo básico de criação de threads:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import threading import time def trabalhador(nome, segundos): """Função executada por cada thread.""" print(f"Thread {nome}: iniciando") time.sleep(segundos) print(f"Thread {nome}: finalizada após {segundos}s") # Criando threads manualmente t1 = threading.Thread(target=trabalhador, args=("A", 2)) t2 = threading.Thread(target=trabalhador, args=("B", 1)) # Iniciando as threads t1.start() t2.start() # Aguardando ambas terminarem t1.join() t2.join() print("Todas as threads finalizaram") # Usando lista de threads threads = [] for i in range(5): t = threading.Thread(target=trabalhador, args=(f"Thread-{i}", i % 3 + 1)) threads.append(t) t.start() for t in threads: t.join() |
Threads executam concorrentemente, não em paralelo real. O GIL alterna entre elas rapidamente, dando a ilusão de simultaneidade. Para I/O, isso é suficiente e muito eficiente.
Sincronização entre threads
Quando threads compartilham dados, podem ocorrer condições de corrida.
Por exemplo, duas threads incrementando a mesma variável simultaneamente.
Isso corrompe o resultado final.
Para evitar isso, use mecanismos de sincronização.
O Lock (mutex) permite que apenas uma thread execute uma seção crítica.
Outros mecanismos incluem RLock, Semaphore e Event.
Quando usar locks? Sempre que múltiplas threads acessarem dados compartilhados.
A voz passiva é usada aqui: “os recursos compartilhados são protegidos por locks”.
Exemplo de contador seguro com lock:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import threading import time # Sem sincronização (INCORRETO) contador_sem_lock = 0 def incrementar_sem_lock(): global contador_sem_lock for _ in range(100000): contador_sem_lock += 1 # Com sincronização (CORRETO) contador_com_lock = 0 lock = threading.Lock() def incrementar_com_lock(): global contador_com_lock for _ in range(100000): with lock: # Entrada na seção crítica contador_com_lock += 1 # Saída automática da seção crítica # Executando sem lock threads_sem = [] for _ in range(5): t = threading.Thread(target=incrementar_sem_lock) threads_sem.append(t) t.start() for t in threads_sem: t.join() print(f"Sem lock: {contador_sem_lock} (esperado: 500000)") # Executando com lock threads_com = [] for _ in range(5): t = threading.Thread(target=incrementar_com_lock) threads_com.append(t) t.start() for t in threads_com: t.join() print(f"Com lock: {contador_com_lock} (esperado: 500000)") # Exemplo de deadlock (evite!) lock_a = threading.Lock() lock_b = threading.Lock() def tarefa_1(): with lock_a: time.sleep(0.01) with lock_b: # Pode causar deadlock com tarefa_2 pass def tarefa_2(): with lock_b: time.sleep(0.01) with lock_a: # Pode causar deadlock com tarefa_1 pass |
O resultado sem lock será imprevisível e geralmente menor que 500000. Com lock, o resultado é sempre correto, porém mais lento. A fórmula do tempo de execução com contenção é: \(T = T_0 + C \times L\) Onde C é o número de aquisições e L é a latência do lock.
Threads vs. outras formas de concorrência
Threads são ótimas para I/O-bound com poucas conexões. Para CPU-bound, prefira multiprocessing (processos separados). Para milhares de conexões simultâneas, asyncio é superior. Quando escolher threading especificamente? Em projetos simples. Também quando você já tem código síncrono e não quer reescrever. Threads são mais fáceis de entender que asyncio. A desvantagem é o GIL e a complexidade de locks. A voz passiva é aplicada: “decisões de arquitetura são baseadas no tipo de tarefa”. Exemplo prático de pool de threads para tarefas I/O:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import threading from concurrent.futures import ThreadPoolExecutor import requests import time urls = [ 'https://httpbin.org/delay/1', 'https://httpbin.org/delay/2', 'https://httpbin.org/delay/0.5', ] * 5 # 15 URLs no total def baixar(url): inicio = time.time() resposta = requests.get(url) return url, resposta.status_code, time.time() - inicio # Usando ThreadPoolExecutor (mais elegante) print("Usando ThreadPoolExecutor:") with ThreadPoolExecutor(max_workers=5) as executor: resultados = executor.map(baixar, urls) for url, status, tempo in resultados: print(f"{url} -> status {status} em {tempo:.2f}s") # Alternativa manual com lista de threads print("\nUsando threads manuais:") threads = [] resultados_manuais = [] def baixar_e_armazenar(url, idx): _, status, tempo = baixar(url) resultados_manuais.append((idx, status, tempo)) for i, url in enumerate(urls): t = threading.Thread(target=baixar_e_armazenar, args=(url, i)) threads.append(t) t.start() for t in threads: t.join() |
ThreadPoolExecutor simplifica o gerenciamento de threads.
Ele reutiliza threads para evitar custo de criação.
Use max_workers entre 5 e 10 para tarefas de rede.
Nunca crie mais de 100 threads simultâneas.
O sistema operacional pode ficar sobrecarregado.
Threads são ferramentas poderosas quando usadas corretamente.
Domine os locks e evite deadlocks.
Sua aplicação ficará mais rápida e responsiva.
Comece com pequenos exemplos e escale gradualmente.