AUTONETOPS

Cover Image for Desmistificando o Async em Python: Um Guia para Automação de Redes

Desmistificando o Async em Python: Um Guia para Automação de Redes

·

5 min read

A programação assíncrona (async/await) em Python é uma das funcionalidades mais poderosas e, ao mesmo tempo, mais mal compreendidas. Para engenheiros de automação de redes, cujas tarefas são dominadas por esperas: esperar por uma conexão SSH, pela resposta de uma API, pelo output de um comando, o async é uma necessidade. Ele promete executar dezenas de tarefas de rede no tempo que levaria para fazer apenas uma.

No entanto, entrar no mundo async pode ser confuso. O que é uma corrotina? Eu preciso mesmo do asyncio? E, a pergunta mais crítica de todas: o que acontece se eu misturar código síncrono e assíncrono?

O Básico: Funções Síncronas vs. Assíncronas

  • Função Síncrona (normal): Quando você a chama, ela executa do início ao fim sem interrupção. Se ela precisa esperar por algo (como uma resposta de rede), todo o seu programa para e espera junto. É como um telefonema: você fica na linha até a conversa acabar.

  • Função Assíncrona (Corrotina): É uma função que pode ser pausada e retomada. Quando ela encontra uma operação de espera (marcada com await), ela diz ao Python: "Ei, isso vai demorar. Pode ir fazendo outra coisa enquanto eu espero". É como enviar uma mensagem de texto: você envia e pode fazer outras coisas até a resposta chegar.

As Peças do Quebra-Cabeça Async

Para o async funcionar, precisamos de três componentes principais:

  1. Corrotinas (async def): São as funções "pausáveis". Você as define com async def.

  2. await: A palavra-chave que de fato pausa a corrotina e devolve o controle, dizendo "estou esperando por este resultado". Você só pode usar await dentro de uma async def.

  3. O Event Loop: É o maestro que orquestra tudo. Ele mantém uma lista de tarefas e executa uma de cada vez. Quando uma tarefa é pausada com await, o event loop imediatamente pega a próxima tarefa da lista e a executa. Ele é o coração do asyncio.

Eu Preciso do asyncio?

Sim e não.

  • Não, você não precisa importar e interagir com todas as partes complexas do asyncio o tempo todo.

  • Sim, você precisa de algo para rodar o event loop e gerenciar as corrotinas. O asyncio é a biblioteca padrão do Python para isso.

Na prática, seu uso do asyncio pode ser tão simples quanto:

  • asyncio.run(main()): O ponto de entrada que inicia o event loop e executa sua corrotina principal main.

  • asyncio.gather(*tasks): Uma forma de executar uma lista de corrotinas concorrentemente e esperar que todas terminem.

Você não precisa construir o event loop manualmente; o asyncio cuida da parte difícil para você.

O Pecado Capital: Chamar Código Bloqueante em uma Corrotina

Esta é a regra mais importante da programação assíncrona: nunca, jamais, chame uma função síncrona e bloqueante dentro de uma corrotina.

Funções bloqueantes são aquelas que fazem o programa esperar, como:

  • time.sleep(5)

  • requests.get(url)

  • Uma chamada de uma biblioteca de rede que não é async, como netmiko.ConnectHandler(...)

O que acontece quando você faz isso?

Você congela o chef de cozinha. A função bloqueante toma controle total do processo e não o devolve até que ela termine. O event loop fica paralisado, incapaz de trocar de tarefa. Todas as outras corrotinas que estavam prontas para rodar ficam esperando, famintas. Todo o benefício da concorrência é perdido.

sequenceDiagram
    participant EventLoop
    participant CorrotinaA
    participant CorrotinaB
    participant FuncaoSincrona

    EventLoop->>CorrotinaA: Executar
    CorrotinaA->>FuncaoSincrona: Chamar time.sleep(5)
    Note over EventLoop,FuncaoSincrona: O EVENT LOOP ESTÁ BLOQUEADO!
    Note over CorrotinaB: Esperando para executar...
    FuncaoSincrona-->>CorrotinaA: Retorna após 5s
    CorrotinaA-->>EventLoop: Finaliza
    EventLoop->>CorrotinaB: Finalmente executa

No diagrama acima, a CorrotinaB só pôde executar depois que a FuncaoSincrona (o time.sleep(5)) liberou o processo, 5 segundos depois. A concorrência foi destruída.

A Solução: asyncio.to_thread()

Então, como usamos bibliotecas síncronas e bloqueantes (como a netmiko, que é essencial para muitos de nós) em um código async?

A resposta é delegar o trabalho bloqueante para um thread separado, liberando o event loop para continuar seu trabalho. Desde o Python 3.9, o asyncio tornou isso incrivelmente fácil com asyncio.to_thread().

asyncio.to_thread(func, *args) pega uma função síncrona func e seus argumentos *args, a executa em um thread separado e retorna um objeto "aguardável" (awaitable). O event loop pode "esperar" por esse objeto sem ser bloqueado.

Caso Prático: Netmiko (Síncrono) + HTTPX (Assíncrono)

Vamos criar um script que, concorrentemente, busca a configuração de um roteador usando netmiko e busca dados de uma API usando httpx.

O jeito ERRADO (bloqueante):

import asyncio
import httpx
from netmiko import ConnectHandler

async def get_config_bloqueante(device): # RUIM!
    print('Iniciando conexão Netmiko...')
    with ConnectHandler(**device) as conn:
        output = conn.send_command('show run')
    print('Conexão Netmiko finalizada.')
    return output

async def get_api_data():
    print('Iniciando chamada de API...')
    async with httpx.AsyncClient() as client:
        await client.get('https://httpbin.org/delay/2') # Simula espera de 2s
    print('Chamada de API finalizada.')

async def main():
    # ... (definição do dispositivo) ...
    await asyncio.gather(
        get_config_bloqueante(device), # Esta chamada irá bloquear tudo!
        get_api_data()
    )

# Se a conexão Netmiko levar 5s, a chamada de API só começará depois disso.

O jeito CERTO (com to_thread):

import asyncio
import httpx
from netmiko import ConnectHandler

# Função síncrona e bloqueante, como deve ser
def get_config_sincrono(device):
    print('Iniciando conexão Netmiko em um thread...')
    with ConnectHandler(**device) as conn:
        output = conn.send_command('show run')
    print('Conexão Netmiko finalizada.')
    return output

async def get_api_data():
    print('Iniciando chamada de API...')
    async with httpx.AsyncClient() as client:
        await client.get('https://httpbin.org/delay/2')
    print('Chamada de API finalizada.')

async def main():
    device = {
    # dados do router
    }

    # Delega a função bloqueante para um thread separado
    netmiko_task = asyncio.to_thread(get_config_sincrono, device)
    api_task = get_api_data()

    await asyncio.gather(netmiko_task, api_task)

asyncio.run(main())

Nesta versão, enquanto get_config_sincrono está rodando em seu próprio thread, o event loop está livre para executar get_api_data concorrentemente. As duas tarefas progridem ao mesmo tempo.

Conclusão

Entender async é entender a arte de não esperar. Para automação de redes, onde o tempo de espera é o maior inimigo, isso é transformador.

Lembre-se das regras de ouro:

  1. Use async/await para tarefas I/O-bound: Interações com rede (APIs, SSH) são o caso de uso perfeito.

  2. Nunca bloqueie o Event Loop: Uma única chamada síncrona bloqueante em uma corrotina destrói toda a vantagem do async.

  3. Use asyncio.to_thread(): É a sua ponte segura para integrar bibliotecas síncronas e legadas em um mundo assíncrono moderno.

Ao dominar esses conceitos, você estará equipado para escrever automações de rede drasticamente mais rápidas, eficientes e escaláveis.


Referências:

  1. RealPython - Async IO in Python: A Complete Walkthrough

  2. Documentação Oficial do asyncio

  3. Documentação do asyncio.to_thread

;