Aula 18 - Programação Assíncrona com asyncio e Multiprocessamento ⚡
Objetivo Pedagógico
Objetivo: Conceber sistemas concorrentes de alto desempenho em Python: compreender o Global Interpreter Lock (GIL), aplicar I/O assíncrono cooperativo com asyncio e contornar restrições de CPU utilizando o módulo multiprocessing e memória compartilhada.
📑 1. Fundamentos Teóricos & Análise Técnica
A concorrência em Python requer uma compreensão precisa de suas restrições arquiteturais, especialmente o Global Interpreter Lock (GIL) do CPython.
A escolha da estratégia correta depende da natureza da carga de trabalho: 1. O Global Interpreter Lock (GIL) e Tarefas CPU-Bound vs. I/O-Bound: - O GIL é um mutex que impede que múltiplas threads nativas executem bytecodes Python simultaneamente dentro do mesmo processo. - Para tarefas limitadas por operações de entrada/saída (I/O-Bound: rede, banco de dados, arquivos), o GIL é liberado durante a espera do socket, tornando threads ou programação assíncrona altamente eficientes. - Para tarefas limitadas por computação pesada (CPU-Bound: criptografia, processamento de imagens, machine learning), threads normais competem pelo GIL sem ganho de paralelismo real. A solução mandatória é o Multiprocessamento (multiprocessing), que instancia múltiplos processos Python independentes, cada um com sua própria memória e seu próprio GIL em núcleos distintos da CPU. 2. Programação Assíncrona Cooperativa (asyncio): - Opera sobre um Loop de Eventos (Event Loop) em uma única thread. - As corrotinas (async def) cedem o controle voluntariamente através da palavra-chave await. Milhares de conexões ativas podem ser mantidas simultaneamente sem a sobrecarga de memória de processos separados.
📐 Arquitetura Conceitual & Diagrama de Fluxo
graph TD
TaskDecision{"Natureza da Carga de Trabalho?"}
TaskDecision -- I/O Bound (Rede, APIs, DB) --> AsyncIO["asyncio Event Loop (1 Thread, Milhares de Conexões)"]
TaskDecision -- CPU Bound (Cálculo, Parsing) --> MultiProc["Multiprocessing Pool (1 Processo por Núcleo Físico)"]
MultiProc --> Core1["Núcleo 1 (Processo Python 1 - GIL Isolado)"]
MultiProc --> Core2["Núcleo 2 (Processo Python 2 - GIL Isolado)"]
MultiProc --> CoreN["Núcleo N (Processo Python N - Paralelismo 100% Real)"]
style TaskDecision fill:#e1f5fe,stroke:#01579b
style AsyncIO fill:#fff3e0,stroke:#e65100
style MultiProc fill:#f3e5f5,stroke:#7b1fa2
style Core1 fill:#e8f5e9,stroke:#2e7d32
style Core2 fill:#e8f5e9,stroke:#2e7d32
style CoreN fill:#e8f5e9,stroke:#2e7d32 🔍 Pilares e Diretrizes Técnicas
Nesta unidade, aprofundamos os seguintes conceitos fundamentais: - Event Loop Não-Bloqueante: Execução leve de milhares de tarefas cooperativas sem chaveamento de contexto no kernel. - Bypass do GIL via Multiprocessamento: Aproveitamento integral de todos os núcleos da CPU para algoritmos matemáticos pesados. - Memória Compartilhada (multiprocessing.shared_memory): Troca rápida de arrays e dados binários entre processos sem o custo de serialização Pickle. - Cancelamento e Timeouts com asyncio.wait_for: Prevenção de corrotinas zumbis que travam o loop de eventos indefinidamente.
🛠️ 2. Implementação Prática em Concorrência em Python, asyncio, GIL e Multiprocessing
Abaixo está a implementação técnica de referência, estruturada com padrões de engenharia de software e foco em robustez:
// hybrid_concurrency.py (Combinação de asyncio para I/O com ProcessPool para CPU-Bound)
import asyncio
import time
from concurrent.futures import ProcessPoolExecutor
def heavy_cpu_task(number: int) -> int:
"""Tarefa pesada de CPU executada em processo isolado fora do GIL."""
count = 0
for i in range(number * 100_000):
count += (i % 3)
return count
async def async_io_fetch(task_id: int):
"""Simulação de I/O assíncrono cooperativo."""
await asyncio.sleep(0.1)
return f"I/O Resultado {task_id}"
async def main():
loop = asyncio.get_running_loop()
print("Iniciando arquitetura híbrida de concorrência...")
start = time.perf_counter()
# 1. Disparar tarefas pesadas de CPU em processos separados
with ProcessPoolExecutor() as process_pool:
cpu_futures = [
loop.run_in_executor(process_pool, heavy_cpu_task, 50)
for _ in range(4)
]
# 2. Concomitantemente, executar dezenas de tarefas assíncronas de I/O
io_coros = [async_io_fetch(i) for i in range(10)]
# 3. Aguardar todas as operações paralelamente
cpu_results, io_results = await asyncio.gather(
asyncio.gather(*cpu_futures),
asyncio.gather(*io_coros)
)
elapsed = time.perf_counter() - start
print(f"Concluído em {elapsed:.2f}s | CPU Processos: {len(cpu_results)} | I/O Tarefas: {len(io_results)}")
if __name__ == '__main__':
asyncio.run(main())
💡 Análise Passo a Passo do Código
- Uso de
run_in_executorcomProcessPoolExecutor: Despacha cálculos intensivos para processos secundários sem congelar o loop de eventos assíncrono. - Função
asyncio.gatherComposta: Sincroniza o retorno de tarefas de computação e operações de rede em uma única instrução concisa. - Isolamento Total do GIL: Permite que os 4 processos utilizem 100% de 4 núcleos distintos de hardware simultaneamente.
🎯 3. Próximos Passos & Sequência Didática
-
Slides da Aula
-
Quiz de Fixação
-
Exercícios Práticos
-
Desafio de Projeto