Multiprocessamento em Python: Paralelismo Real

python

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:

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:

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:

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.

Multithreading em Python

python

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:

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:

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:

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.