Multiprocessamento cria múltiplos processos independentes.
Cada processo tem seu próprio interpretador Python e memória.
Primeiramente, isso contorna a limitação do GIL completamente.
Por exemplo, 4 processos podem executar em 4 núcleos de CPU simultaneamente.
Além disso, processos não compartilham memória por padrão.
A voz passiva é usada aqui: “dados são copiados entre processos via serialização”.
Quando utilizar multiprocessing? Em tarefas com CPU intensiva.
Por exemplo, processamento de imagens, cálculos matemáticos ou criptografia.
Também é útil para explorar todo o potencial da máquina.
Python oferece o módulo multiprocessing para isso.
Vamos explorar criação, comunicação e boas práticas.
Três subtítulos guiarão você pelo paralelismo real.
Ao final, você dominará o multiprocessamento em Python.
Criando e gerenciando processos
O módulo multiprocessing tem interface similar ao threading.
Use Process para criar processos individuais.
O método start() inicia o processo e join() espera terminar.
Para múltiplas tarefas, use Pool para gerenciar um conjunto de processos.
Quando usar criação manual? Em poucos processos com lógica específica.
A voz passiva é aplicada: “os argumentos são passados via args“.
Exemplo básico de criação de processos:
|
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 multiprocessing import time import os def trabalhador(nome, segundos): """Função executada por cada processo.""" pid = os.getpid() print(f"Processo {nome} (PID: {pid}) iniciando") time.sleep(segundos) print(f"Processo {nome} finalizado após {segundos}s") return f"Resultado de {nome}" if __name__ == "__main__": print(f"Processo principal PID: {os.getpid()}") # Criando processos manualmente p1 = multiprocessing.Process(target=trabalhador, args=("A", 2)) p2 = multiprocessing.Process(target=trabalhador, args=("B", 1)) p1.start() p2.start() p1.join() p2.join() # Usando Pool para múltiplos processos print("\n=== Usando Pool ===") with multiprocessing.Pool(processes=4) as pool: # Mapear uma lista de argumentos para a função args_lista = [(f"Task-{i}", i % 3 + 1) for i in range(6)] resultados = pool.starmap(trabalhador, args_lista) print(f"Resultados: {resultados}") |
Cada processo tem seu próprio PID (identificador único).
O Pool gerencia automaticamente quantos processos rodam simultaneamente.
Isso é mais eficiente que criar centenas de processos manuais.
Comunicação entre processos
Processos não compartilham memória como threads.
Portanto, precisamos de mecanismos especiais para comunicação.
O Queue permite trocar dados entre processos de forma segura.
O Pipe oferece comunicação bidirecional entre dois processos.
Já o Value e Array compartilham memória com locks.
Quando usar cada um? Queue para produtor-consumidor.
Pipe para comunicação simples entre dois processos.
A voz passiva é aplicada: “os dados são serializados com pickle automaticamente”.
Exemplo de comunicação com Queue:
|
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 |
import multiprocessing import time def produtor(queue, itens): """Produz itens e coloca na fila.""" for item in itens: print(f"Produzindo: {item}") queue.put(item) time.sleep(0.5) queue.put(None) # Sinal de fim def consumidor(queue, nome): """Consome itens da fila.""" while True: item = queue.get() if item is None: break print(f"{nome} consumiu: {item}") time.sleep(0.3) if __name__ == "__main__": queue = multiprocessing.Queue() itens = ["Item1", "Item2", "Item3", "Item4", "Item5"] p_produtor = multiprocessing.Process(target=produtor, args=(queue, itens)) p_consumidor1 = multiprocessing.Process(target=consumidor, args=(queue, "Consumidor-1")) p_consumidor2 = multiprocessing.Process(target=consumidor, args=(queue, "Consumidor-2")) p_produtor.start() p_consumidor1.start() p_consumidor2.start() p_produtor.join() p_consumidor1.join() p_consumidor2.join() # Exemplo com Value (memória compartilhada) print("\n=== Memória Compartilhada ===") contador = multiprocessing.Value('i', 0) lock = multiprocessing.Lock() def incrementar(): with lock: contador.value += 1 processos = [] for _ in range(10): p = multiprocessing.Process(target=incrementar) processos.append(p) p.start() for p in processos: p.join() print(f"Valor final do contador: {contador.value}") |
Queues são ideais para padrões produtor-consumidor. Valores compartilhados exigem locks para evitar condições de corrida. A comunicação entre processos tem overhead, então use apenas quando necessário.
Pool e map para paralelismo de dados
A função pool.map() é a maneira mais fácil de paralelizar.
Ela divide uma lista de dados entre os processos disponíveis.
Cada processo aplica a mesma função a um subconjunto dos dados.
Quando usar map? Em problemas de processamento de listas grandes.
Por exemplo, aplicar uma função a cada elemento de 1 milhão de itens.
A voz passiva é aplicada: “os resultados são coletados automaticamente”.
Exemplo prático com processamento paralelo de números:
|
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 |
import multiprocessing import time import math def processar_numero(n): """Função CPU-intensiva para testar paralelismo.""" # Calcula muitos primos para simular trabalho pesado primos = [] for i in range(2, min(10000, n)): eh_primo = True for j in range(2, int(math.sqrt(i)) + 1): if i % j == 0: eh_primo = False break if eh_primo: primos.append(i) return len(primos) if __name__ == "__main__": numeros = list(range(10000, 10100)) # 100 números # Versão sequencial inicio_seq = time.time() resultados_seq = [processar_numero(n) for n in numeros] tempo_seq = time.time() - inicio_seq print(f"Sequencial: {tempo_seq:.2f}s") # Versão com multiprocessing inicio_par = time.time() with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool: resultados_par = pool.map(processar_numero, numeros) tempo_par = time.time() - inicio_par print(f"Paralelo (com {multiprocessing.cpu_count()} CPUs): {tempo_par:.2f}s") print(f"Aceleração: {tempo_seq / tempo_par:.2f}x") # Verificando se os resultados são iguais assert resultados_seq == resultados_par print("Resultados idênticos!") # Usando map com múltiplos argumentos (starmap) def potencia(base, expoente): return base ** expoente argumentos = [(2, 10), (3, 8), (5, 5), (7, 4)] with multiprocessing.Pool() as pool: resultados_pot = pool.starmap(potencia, argumentos) print(f"Potências: {resultados_pot}") # Usando map_async para não-bloqueante with multiprocessing.Pool() as pool: resultado_async = pool.map_async(processar_numero, numeros[:10]) print("Fazendo outra coisa enquanto processa...") resultados_async = resultado_async.get(timeout=30) print(f"Resultados async: {resultados_async[:3]}...") |
A aceleração deve ser próxima ao número de núcleos da CPU. Para 4 núcleos, espere cerca de 3.5x de ganho. A fórmula teórica é a lei de Amdahl: \(S = \frac{1}{(1 – P) + \frac{P}{N}}\) Onde P é a fração paralelizável e N é o número de núcleos. Multiprocessamento é a ferramenta certa para CPU-bound. Combine com boas práticas e evite overhead desnecessário. Seu código rodará muito mais rápido em máquinas modernas.