Async/await: introdução
Esta aula introduz programação assíncrona em Rust, explicando Futures, async/await, o runtime Tokio e as diferenças entre threads e tarefas assíncronas. Você aprenderá a escrever código não bloqueante e eficiente.
Programação assíncrona é essencial para construir aplicações eficientes, especialmente em I/O. Em Rust, ela é implementada através de Futures e das palavras-chave async e .await. Diferente de threads, que são gerenciadas pelo sistema operacional, tarefas assíncronas são leves e cooperativas, permitindo escalar para milhares de conexões simultâneas.
Nesta aula, você entenderá os conceitos fundamentais: o que é um Future, como definir funções assíncronas, como aguardar resultados com .await e como executar essas tarefas usando um runtime como Tokio. Também compararemos com threads para escolher a abordagem certa.
Futures
Um Future em Rust representa um valor que pode não estar disponível ainda. É uma abstração para computação que pode ser concluída em algum momento futuro. A trait Future possui um método poll que verifica se o valor está pronto. Enquanto não está, a tarefa pode ceder o controle para outras tarefas.
Futures são lazy: eles não fazem nada até que sejam executados por um runtime. Você pode combiná-los usando combinadores como and_then, map, ou mais comumente, com async/.await.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct MyFuture;
impl Future for MyFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(42)
}
}
// Exemplo de uso (requer um executor)
// let fut = MyFuture;
// let result = block_on(fut); // 42async fn e .await
A palavra-chave async transforma uma função em uma função que retorna um Future. Dentro de uma função async, você pode usar .await para aguardar a conclusão de outro Future, sem bloquear a thread atual. O estado da função é salvo e restaurado pelo compilador.
async fn fetch_data(url: &str) -> String {
// Simula uma operação assíncrona
format!("Dados de {}", url)
}
async fn process() {
let data = fetch_data("https://exemplo.com").await;
println!("{}", data);
}
// Para executar, precisamos de um runtime:
// #[tokio::main]
// async fn main() {
// process().await;
// }O .await suspende a execução da função atual até que o Future esteja pronto. Enquanto isso, a thread pode executar outras tarefas, tornando o código eficiente.
Runtimes (tokio)
Um runtime é o executor que gerencia a execução de Futures. O mais popular em Rust é o Tokio. Ele fornece um agendador, temporizadores, I/O assíncrono e muito mais. Para usar Tokio, adicione ao Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }Exemplo de um programa Tokio:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Início");
sleep(Duration::from_secs(1)).await;
println!("Fim após 1 segundo");
}Tokio usa um agendador work-stealing para distribuir tarefas entre threads, maximizando o uso da CPU. Você pode criar tarefas com tokio::spawn:
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
println!("Tarefa assíncrona");
42
});
let result = handle.await.unwrap();
println!("Resultado: {}", result);
}Diferença de threads
Threads são gerenciadas pelo sistema operacional e têm custo de criação e troca de contexto alto. Cada thread consome uma pilha separada (normalmente 1 MB ou mais). Já tarefas assíncronas são leves: cada uma ocupa apenas alguns bytes no heap para o estado da máquina de estados, e a troca é cooperativa (sem intervenção do SO).
Use threads para tarefas CPU-bound (computação pesada) que precisam de paralelismo real. Use async para tarefas I/O-bound (rede, disco) onde você espera muitos bloqueios. Async permite escalar para milhares de conexões com poucas threads.
| Característica | Threads | Async/Tarefas |
|---|---|---|
| Custo de criação | Alto (pilha ~1MB) | Baixo (alguns bytes) |
| Troca de contexto | Preemptiva (SO) | Cooperativa (no .await) |
| Escalabilidade | Centenas a milhares | Dezenas de milhares |
| Uso ideal | CPU-bound | I/O-bound |
Em Rust, você pode combinar ambos: usar async para I/O e threads para processamento pesado, com comunicação via canais.
Boas práticas
- Não bloqueie o runtime com chamadas síncronas (ex:
std::thread::sleep). Usetokio::time::sleep. - Evite
block_ondentro de código assíncrono; prefira.await. - Para I/O, use bibliotecas assíncronas como
tokio::fs,tokio::net. - Mantenha funções
asynccurtas e focadas; combine-as comjoin!ouselect!.
Referências
- Livro de Rust - Projeto final: servidor web
- Tutorial oficial Tokio
- Async Book do Rust
- Documentação da trait Future
- Documentação da crate Tokio
Exercícios
Crie uma função assíncrona
ler_arquivoque simula a leitura de um arquivo comtokio::time::sleeppor 2 segundos e retorna uma String. Escreva um programa que a chame e imprima o resultado.✓ Resposta:use tokio::time::{sleep, Duration}; async fn ler_arquivo() -> String { sleep(Duration::from_secs(2)).await; String::from("Conteúdo do arquivo") } #[tokio::main] async fn main() { let conteudo = ler_arquivo().await; println!("{}", conteudo); }Explique a diferença entre
thread::spawnetokio::spawn. Dê um exemplo onde cada um é mais adequado.✓ Resposta:thread::spawncria uma thread do SO, cara e com pilha grande. Ideal para tarefas CPU-bound.tokio::spawncria uma tarefa assíncrona leve, ideal para I/O-bound. Exemplo: servidor web (async) vs processamento de imagens (thread).Escreva um programa que usa
tokio::join!para executar duas funções assíncronas concorrentemente: uma que espera 1 segundo e outra que espera 2 segundos. Meça o tempo total (deve ser ~2 segundos).✓ Resposta:use tokio::time::{sleep, Duration}; async fn tarefa1() { sleep(Duration::from_secs(1)).await; println!("Tarefa 1 concluída"); } async fn tarefa2() { sleep(Duration::from_secs(2)).await; println!("Tarefa 2 concluída"); } #[tokio::main] async fn main() { let inicio = std::time::Instant::now(); tokio::join!(tarefa1(), tarefa2()); println!("Tempo total: {:?}", inicio.elapsed()); }O que acontece se você chamar
.awaitem um Future que retornaPoll::Pendingpara sempre? Como evitar isso?✓ Resposta:A tarefa ficará suspensa para sempre, nunca retornando. Para evitar, use timeouts com
tokio::time::timeoutou garanta que o Future seja completado eventualmente.Implemente uma função assíncrona que recebe um vetor de URLs (strings) e baixa o conteúdo de cada uma concorrentemente usando
tokio::spawn. Usereqwestpara fazer as requisições (adicione a dependência).✓ Resposta:use tokio::task; async fn baixar_url(url: String) -> String { let resp = reqwest::get(&url).await.unwrap(); resp.text().await.unwrap() } #[tokio::main] async fn main() { let urls = vec![ "https://example.com".to_string(), "https://httpbin.org/get".to_string(), ]; let mut handles = vec![]; for url in urls { handles.push(task::spawn(baixar_url(url))); } for handle in handles { let conteudo = handle.await.unwrap(); println!("Conteúdo: {}", &conteudo[..50]); } }