Alura > Cursos de Programação > Cursos de > Conteúdos de > Primeiras aulas do curso Praticando python: programação assíncrona

Praticando python: programação assíncrona

Programação assíncrona - Entendendo a Programação Síncrona e Assíncrona

Boas-vindas! Meu nome é Laís Urano, sou instrutora na escola de Programação da Alura, e irei te acompanhar ao longo dessa jornada de aprendizagem.

Audiodescrição: Laís se descreve como uma mulher parda, de olhos castanho-claros, e cabelos cacheados, longos e castanho-escuros com mechas azuis. Ela usa cordões prateados no pescoço, batom vermelho nos lábios, delineado preto nos olhos, e está sentada em uma cadeira rosa em frente a uma parede clara iluminada em gradiente azul-escuro e verde-claro. À direita de Laís, há um microfone branco sobre um braço articulado preto.

Entendendo programação síncrona e assíncrona

O que é programação síncrona?

Até o momento, durante a formação Praticando Python, trabalhamos com a programação síncrona, um modelo de execução em que tarefas são realizadas de forma sequencial, ou seja, cada operação deve ser concluída para que a próxima se inicie.

Abaixo, temos um exemplo que utiliza a biblioteca time:

import time

def tarefa(numero):
    print(f"Iniciando tarefa {numero}.")
    time.sleep(2)
    print(f"Tarefa {numero} concluída!")

tarefa(1)
tarefa(2)
tarefa(3)

No código, há uma função tarefa() que recebe numero como parâmetro. Nela, existem dois print(): o primeiro indica o início da tarefa; o segundo, um time.sleep(), ou seja, uma pausa de 2 segundos; e outro print() indicando que a tarefa foi concluída.

Além disso, realizamos três chamadas de tarefa():

Quando executamos esse programa, iniciamos a tarefa 1, concluímos, depois iniciamos a tarefa 2, concluímos, e assim por diante. Ou seja, para que a tarefa 2 inicie, a tarefa 1 precisa ter sido iniciada e concluída. Da mesma forma, para que a tarefa 3 inicie, a tarefa 2 precisa também ter sido iniciada e concluída. Isso ocorre de forma sequencial, conforme dito anteriormente.

No entanto, esse modelo pode ser considerado inapropriado em alguns contextos.

O que é programação assíncrona?

Em contextos mais específicos, utilizamos a programação assíncrona, que permite executar várias tarefas ao mesmo tempo, sem que uma precise esperar a outra terminar para iniciar.

Nesse tipo de programação, temos duas possibilidades:

  1. A primeira é a concorrência, na qual alternamos tão rapidamente entre as tarefas executadas, que passamos a impressão de que elas rodam ao mesmo tempo. Isso ocorre, por exemplo, quando utilizamos tarefas de entrada e saída de dados, como chamadas a uma API ou acesso a um banco de dados;
  2. A segunda é o paralelismo, em que múltiplas tarefas são executadas simultaneamente. Isso exige que a máquina tenha uma potência superior para lidar com programas de cálculos pesados e processamento de dados.

Como isso se aplica a um contexto real?

Imagine o cenário de atendimento em um restaurante.

De forma síncrona, o garçom ou a garçonete atendem somente um cliente por vez, pegando o próximo pedido apenas quando o primeiro terminar. Ou seja, o primeiro cliente chega, faz o pedido, espera, come, paga, sai, e só então o segundo cliente é atendido.

Por outro lado, de forma assíncrona, essa pessoa anotaria o pedido do primeiro cliente e, enquanto a cozinha prepara esse pedido, ela atenderia outro cliente.

Em uma abordagem assíncrona, temos duas possibilidades:

  1. A concorrência, na qual um garçom atende vários clientes e alterna entre eles, mas continua sozinho no trabalho;
  2. Ou o paralelismo, em que vários garçons atendem diferentes clientes simultaneamente em mesas distintas no restaurante.

Quando usar programação síncrona ou assíncrona?

A programação síncrona deve ser usada quando o fluxo do programa for simples e sequencial.

Por exemplo: quando precisamos calcular o desconto de uma compra, ou quando a lógica do código depende da execução ordenada das operações. Imagine que acabamos de realizar uma compra em um site. Nesse cenário, é necessário que o pagamento seja confirmado antes do pedido ser enviado.

A síncrona também é adequada quando há pouca ou nenhuma espera por entrada e saída de dados (I/O), ou quando o código é puramente computacional e pesado. Além disso, utilizamos essa programação quando a simplicidade do código é mais importante que a velocidade, como ao criar um script simples de automação para renomear um arquivo.

Por outro lado, a programação assíncrona é indicada em contextos onde múltiplas tarefas dependem de I/O, como ao trabalhar com uma API externa ou consultar um banco de dados.

Também utilizamos a programação assíncrona quando precisamos lidar com várias conexões simultâneas, como em um chatbot que precisa interagir com vários clientes ao mesmo tempo, ou quando o sistema precisa melhorar o tempo de resposta.

Além disso, a programação assíncrona é útil quando as tarefas podem ser executadas paralelamente, sem depender umas das outras ou interferir entre si.

A escolha entre programação síncrona e assíncrona pode ser feita com base na lista de critérios mencionados ou por um senso crítico, considerando a pessoa desenvolvedora do projeto ou a empresa responsável pelo desenvolvimento.

Por exemplo: no envio de e-mails de confirmação, podemos optar por uma resposta assíncrona. Se nos cadastramos em um site e é solicitado um e-mail de confirmação, poderemos utilizar o sistema apenas após confirmar o e-mail. No entanto, se o acesso for assíncrono, o e-mail é enviado em segundo plano, permitindo utilizar o sistema.

No final, a escolha depende da pessoa desenvolvedora.

Conclusão

Agora que entendemos como funcionam e como podemos utilizar a programação síncrona e assíncrona, podemos aplicar esse conhecimento nos nossos projetos.

Faremos isso a seguir. Nos encontramos no próximo vídeo!

Programação assíncrona - Desenvolvendo com Programação Assíncrona

Agora que compreendemos a diferença entre programação síncrona e assíncrona, para aplicar a programação assíncrona efetivamente em nossos projetos, utilizaremos a biblioteca asyncio, uma biblioteca padrão do Python para escrita de código assíncrono.

Desenvolvendo com programação assíncrona

Conhecendo a biblioteca asyncio

Nesse caso, não precisamos fazer nenhuma instalação, apenas chamar a biblioteca.

A biblioteca asyncio é utilizada para operações como chamadas de API sem bloquear o programa, leitura e escrita de arquivos ou bancos de dados, e criação de servidores e bots que lidam com conexões simultâneas. Todos esses casos estão inseridos no contexto de programação assíncrona.

O que são awaitables?

Para começarmos a entender a programação assíncrona em nosso código, precisamos compreender certos termos. O primeiro deles é awaitables, ou aguardáveis, que são objetos que podem ser esperados com await. O await se traduz literalmente para "aguarde", existindo três tipos:

Abaixo, trouxemos o exemplo de um código que simula a execução de uma tarefa, com um print() de "Início." e "Fim." dentro de uma função chamada corrotina().

import asyncio

async def corrotina():
    print("Início.")
    await asyncio.sleep(2)
    print("Fim.")

asyncio.run(corrotina())

No escopo da função, utilizamos await asyncio.sleep(2). Fizemos a chamada da biblioteca asyncio e usamos sleep(), agora não mais da biblioteca time, mas da asyncio.

Esse sleep() específico não irá parar o código por dois segundos, mas sim aguardar o código por dois segundos. Por fim, executamos a função corrotina().

O que são coroutines?

O que é uma corrotina? Como ela funciona? A corrotina, ou coroutine, é uma função especial que pode ser pausada e retomada durante sua execução. A função corrotina() é um aguardável e precisa do await. Para definir uma corrotina, que seria basicamente uma função assíncrona, utilizamos o termo async antes da palavra reservada def, que define a função.

Com isso, não se trata mais de uma função comum, mas de uma corrotina, que assume esse nome e papel. Ao executar corrotina(), recebemos as saídas "Início." e "Fim.". Para executar a corrotina, precisamos usar run(), pois ela não é identificada como função comum, mas assíncrona.

Executando uma coroutine

Para executar a função corrotina() ou qualquer outra função assíncrona, utilizamos o método run() ou diretamente o await. Observe o exemplo de código a seguir:

import asyncio

async def corrotina(numero):
    print(f"Iniciando tarefa {numero}.")
    await asyncio.sleep(2)
    print(f"Tarefa {numero} concluída!")

async def main():
    await corrotina(1)
    await corrotina(2)

asyncio.run(main())

Nesse novo contexto, podemos simular a execução de uma tarefa definindo uma função main(). Essa função aguardará a corrotina(1) e a corrotina(2), chamando no método run() a função main() para executar todo o projeto.

Na função que simula a ação da tarefa, iniciamos a tarefa 1, concluímos a tarefa 1, iniciamos a tarefa 2 e concluímos a tarefa 2. Isso deveria ser uma concorrência, mas não acontece, pois dentro da função main(), aguardamos separadamente duas corrotinas. Executamos primeiro uma ação, que é aguardar corrotina(1), e depois aguardamos corrotina(2).

O que são tasks?

Para que as corrotinas trabalhem simultaneamente com concorrência, precisamos utilizar as tasks, ou tarefas, que são objetos que executam a corrotina de forma concorrente, permitindo que várias sejam executadas juntas. Abaixo, temos um exemplo do uso de tarefas em código:

import asyncio

async def corrotina(numero):
    print(f"Iniciando tarefa {numero}.")
    await asyncio.sleep(2)
    print(f"Tarefa {numero} concluída!")

async def main():
    tarefa1 = asyncio.create_task(corrotina(1))
    tarefa2 = asyncio.create_task(corrotina(2))
    await tarefa1
    await tarefa2

asyncio.run(main())

Para trabalhar com uma tarefa, podemos utilizar create_task() da própria biblioteca asyncio. No escopo de main(), alteramos o código para criar uma tarefa utilizando asyncio.create_task().

Na primeira função create_task(), associamos a tarefa à corrotina(1). Depois, criamos uma tarefa2 associada à corrotina(2), que será a mesma corrotina, mas com valores diferentes.

Em seguida, utilizamos o await para executar não a corrotina, mas a tarefa, com await tarefa1 e depois await tarefa2. Nesse momento, iniciamos as duas tarefas simultaneamente.

Como isso acontece quando entramos na corrotina? Primeiramente, iniciamos a tarefa1, entramos e iniciamos a tarefa1, e começamos a aguardar no sleep().

Enquanto aguardamos, verificamos se há outra tarefa que precisa ser executada, que neste caso é a tarefa2. Assim, enquanto aguardamos a tarefa1, iniciamos a tarefa2 simultaneamente. Como a tarefa1 foi iniciada primeiro, será concluída primeiro, pois as tarefas têm o mesmo tempo.

O que são futures?

Existem casos na programação assíncrona em que precisamos lidar com valores chamados de futuros, ou futures. Futuros são valores indefinidos que se definem no futuro, ou seja, algo que ainda assumirá um valor desconhecido. Eles são utilizados na integração de APIs de baixo nível.

Para utilizar futuros em código, usamos asyncio.Future(). No escopo, percebemos algumas alterações. Novamente, este projeto funcionará como uma simulação de tarefa:

import asyncio

async def corrotina(futuro):
    print("Início.")
    await asyncio.sleep(2)
    futuro.set_result("Fim.")

async def main():
    futuro = asyncio.Future()
    asyncio.create_task(corrotina(futuro))
    resultado = await futuro
    print(resultado)

asyncio.run(main())

Na função main(), definimos futuro como uma variável. Esta variável recebe o método asyncio.Future(). No momento em que criamos a tarefa (create_task()) e passamos a corrotina(), passamos também futuro para corrotina().

Na função corrotina(), há um print() de "Início."; o await, aguardável de async; e o sleep(). Depois, utilizamos futuro.set_result() com o valor "Fim.", ou seja, executamos a tarefa e definimos o valor do futuro. Feito isso, como já executamos a função assíncrona, que aguarda para finalizar, ela recebe o valor do futuro na variável resultado.

No momento em que imprimimos resultado, ele define a resposta de futuro. O primeiro print() recebido na saída será o "Início.", que vem junto com a execução da função corrotina(), ou seja, print("Início."). Enquanto aguardamos, ele só executará set_result("Fim.") após informarmos o resultado do futuro e passarmos esse resultado para a variável resultado.

Para ilustrar melhor, vamos imaginar outro cenário:

import asyncio

async def corrotina1(futuro):
    print("Tarefa 1 iniciada.")
    await asyncio.sleep(2)
    futuro.set_result("Resultado da Tarefa 1")
    print("Tarefa 1 finalizada.")

async def corrotina2(futuro):
    print("Tarefa 2 iniciada, aguardando o futuro.")
    resultado = await futuro
    print("Tarefa 2 finalizada com o resultado:", resultado)

# código omitido

No código acima, temos duas corrotinas em execução simultaneamente, em que a primeira executa certa tarefa, mas a segunda precisa do resultado da primeira. Nesse caso, iremos trabalhar em determinada operação, e quando tivermos o resultado dessa operação, passaremos para a segunda.

A corrotina1(), por exemplo, inicia a tarefa 1; define o resultado do futuro da corrotina, que será o resultado da tarefa; e depois imprime que a tarefa 1 foi finalizada.

Já a tarefa 2 é iniciada, mas aguarda o futuro com await. No momento em que o valor do resultado for definido, atribuímos esse resultado à variável resultado declarada abaixo, e finalizamos a execução da função corrotina2().

Agora, vamos verificar esse processo na função main():

# código omitido

async def main():
    futuro = asyncio.Future()
    tarefa1 = asyncio.create_task(corrotina1(futuro))
    tarefa2 = asyncio.create_task(corrotina2(futuro))

    await tarefa1
    await tarefa2

asyncio.run(main())

Em main(), definimos o futuro; chamamos a tarefa1, que recebe o futuro; e também a tarefa2, executando ambas com a função asyncio.create_task(). No final, temos a tarefa1 iniciada, bem como a tarefa2, mas esta última aguardando o futuro. Após resolver o problema da tarefa1 e finalizar, passamos para a tarefa2, finalizando com o resultado da tarefa1.

Executando múltiplas tasks

Na programação assíncrona, precisamos saber como executar múltiplas tarefas, conforme verificado anteriormente com a função create_task().

Para isso, utilizamos tanto essa função quanto a gather(), responsável por unir.

import asyncio

async def corrotina(nome, tempo):
    print(f"Tarefa {nome} iniciada.")
    await asyncio.sleep(tempo)
    print(f"Tarefa {nome} concluída.")

async def main():
    await asyncio.gather(
        corrotina("1",2),
        corrotina("2",3),
        corrotina("3",1),
    )

asyncio.run(main())

O gather() pode substituir a função create_task(), onde a corrotina() de tarefa recebe o nome e o tempo, que varia de acordo com cada corrotina. Na função main(), temos um await juntando todas essas corrotinas. As corrotinas 1, 2 e 3 possuem tempos de execução diferentes.

Quando executamos a função main() com o gather(), podemos verificar que as três tarefas são executadas e iniciadas automaticamente juntas. No entanto, a tarefa 3 foi concluída primeiro, pois enquanto era aguardada no processo de sleep(), a tarefa concluiu seu tempo, que era mais rápido. Assim, a tarefa 3 foi concluída em 1 segundo, a 1 em 2 segundos, e a 2 em 3 segundos.

Relembrando o projeto síncrono

Podemos verificar como isso aconteceu no projeto síncrono inicial, onde utilizamos import time para trabalharmos com a biblioteca de tempo.

import time

def tarefa(numero):
    print(f"Iniciando tarefa {numero}.")
    time.sleep(2)
    print(f"Tarefa {numero} concluída!")

tarefa(1)
tarefa(2)
tarefa(3)

Temos a função tarefa(), que inicia a tarefa; depois pausamos o código por 2 segundos (sleep(2)); e concluímos a tarefa, executando três tarefas de modo sequencial.

Relembrando o projeto assíncrono

Quando adaptamos isso para um projeto assíncrono, substituímos a biblioteca time pela biblioteca asyncio, e definimos a função tarefa(), antes comum, como uma função assíncrona (async), determinando-a agora como uma corrotina. Sendo uma corrotina, podemos passá-la para uma função assíncrona também, que é a main com await, gather, e executando essas três tarefas, as três corrotinas, simultaneamente. Assim, iniciamos e concluímos as tarefas 1, 2 e 3.

import asyncio

async def tarefa(numero):
    print(f"Iniciando tarefa {numero}.")
    await asyncio.sleep(2)
    print(f"Tarefa {numero} concluída!")

async def main():
    await asyncio.gather(tarefa(1), tarefa(2), tarefa(3))

asyncio.run(main())

Conclusão

Observação: para conhecer mais sobre a biblioteca asyncio, recomendamos consultar a documentação oficial do Python, onde é possível verificar módulos e pontos mais específicos para aplicar a programação assíncrona em seus projetos.

Realize as atividades do curso para praticar seus conhecimentos. Aproveite a comunidade da Alura no Discord e o fórum caso tenha dúvidas ou queira discutir o conteúdo. Além disso, compartilhe seus aprendizados nas redes sociais com #AprendizadoAlura.

Nos encontramos em uma próxima oportunidade!

Sobre o curso Praticando python: programação assíncrona

O curso Praticando python: programação assíncrona possui 17 minutos de vídeos, em um total de 15 atividades. Gostou? Conheça nossos outros cursos de em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas