Entendendo Async e Await em Rust
A programação assíncrona permite que seu programa execute múltiplas operações sem bloquear a thread principal. Ao contrário de linguagens como JavaScript, Rust não oferece async por padrão — você precisa de um runtime como Tokio para executar código assíncrono. A sintaxe async cria uma função que retorna uma Future, um objeto que representa um valor que será computado em algum momento no futuro. O await pausa a execução até que essa Future seja resolvida.
Compreender a diferença entre código síncrono (bloqueante) e assíncrono (não-bloqueante) é fundamental. Uma chamada de rede síncrona congela toda a aplicação enquanto aguarda a resposta. Com async/await, você pode atender múltiplos requests simultaneamente usando uma única thread, tornando a aplicação muito mais eficiente. Rust garante segurança com seu sistema de tipos — as Futures são Send e Sync quando apropriado, prevenindo bugs de concorrência em tempo de compilação.
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Iniciando...");
wait_and_print().await;
println!("Finalizado!");
}
async fn wait_and_print() {
sleep(Duration::from_secs(2)).await;
println!("Acordei após 2 segundos!");
}
Futures e o Padrão de Execução
Uma Future em Rust é um trait que descreve uma computação assíncrona. Diferente de Promises em JavaScript, Futures em Rust são lazy — não executam até serem "polled" (consultadas) pelo runtime. Quando você chama uma função async, ela retorna uma Future não iniciada. Apenas quando você a aguarda com .await é que ela começa a execução sob demanda.
O ciclo de vida de uma Future passa por polling repetido até estar pronta (Poll::Ready) ou ainda pendente (Poll::Pending). O runtime gerencia esse polling para você. Isso é crucial: você não precisa entender o mecanismo interno de polling para usar async/await, mas entender que Futures são lazy ajuda a escrever código correto.
use futures::future::join_all;
async fn fetch_user(id: u32) -> String {
println!("Buscando usuário {}", id);
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
format!("Usuário {}", id)
}
#[tokio::main]
async fn main() {
let futures = vec![
fetch_user(1),
fetch_user(2),
fetch_user(3),
];
let results = join_all(futures).await;
println!("Resultados: {:?}", results);
}
Trabalhando com Múltiplas Tasks
Para executar operações realmente concorrentes, use tokio::spawn para criar tasks (tarefas) que rodam independentemente. Ao contrário de threads do SO, tasks são muito leves e você pode criar milhares delas. Cada task é uma Future que executa no runtime de Tokio, compartilhando a mesma ou as mesmas threads.
A diferença entre await simples e spawn é importante: await pausa o ponto atual aguardando um resultado sequencial. spawn lança a task imediatamente e continua — você obtém um JoinHandle para aguardar o resultado depois. Para sincronizar múltiplas tasks, use join! do Tokio ou select! para aguardar a primeira que terminar.
#[tokio::main]
async fn main() {
let handle1 = tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
"Task 1 completa"
});
let handle2 = tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"Task 2 completa"
});
let result1 = handle1.await.unwrap();
let result2 = handle2.await.unwrap();
println!("{}, {}", result1, result2);
}
Tratamento de Erros em Código Assíncrono
Erros em async funcionam como em código síncrono: Result<T, E> é o padrão. A diferença é que você frequentemente lida com erros em múltiplas operações concorrentes. Se uma task falhar, outras continuam — você precisa decidir se quer falhar rápido ou coletar todos os resultados e tratá-los depois.
Para operações críticas onde um erro deve parar tudo, use o operador ? normalmente. Para coletar resultados parciais, itere sobre os JoinHandles e trate cada Result. Sempre use unwrap() com cautela em código de produção — prefira logging e propagação adequada de erros.
use tokio::fs;
async fn read_file(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path).await
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = read_file("arquivo.txt").await?;
println!("Conteúdo: {}", content);
Ok(())
}
Conclusão
Você aprendeu que async/await em Rust permite escrever código não-bloqueante que se parece com código síncrono, mantendo segurança em tempo de compilação. Futures são lazy e executadas sob polling — não confunda com threads. Use tokio::spawn para paralelismo real, mas lembre-se que await sequencial ainda é útil quando você precisa que uma operação termine antes da próxima começar. Finalmente, Rust força você a tratar erros explicitamente em código assíncrono, o que previne bugs sutis encontrados em outras linguagens. Pratique combinando spawn, join! e select! para dominar padrões de concorrência.