Pular para conteúdo

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

  1. Uso de run_in_executor com ProcessPoolExecutor: Despacha cálculos intensivos para processos secundários sem congelar o loop de eventos assíncrono.
  2. Função asyncio.gather Composta: Sincroniza o retorno de tarefas de computação e operações de rede em uma única instrução concisa.
  3. 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