Async/await em Rust: fundamentos
1. O Modelo Assíncrono em Rust
1.1. Programação concorrente vs. assíncrona
A programação concorrente tradicional utiliza threads do sistema operacional para executar múltiplas tarefas simultaneamente. Cada thread possui sua própria pilha e contexto, o que permite execução paralela real em CPUs multi-core. No entanto, threads são caras: cada uma consome megabytes de memória para a pilha, e a alternância entre elas (context switching) tem custo significativo.
A programação assíncrona, por outro lado, permite que uma única thread gerencie múltiplas tarefas de forma cooperativa. Quando uma tarefa aguarda uma operação de I/O, ela "suspende" sua execução e cede o controle para outra tarefa. Isso elimina a sobrecarga de criação de threads e reduz drasticamente o consumo de memória.
1.2. O papel do Future
Em Rust, um Future representa um valor que pode ainda não estar disponível. É a unidade fundamental de computação assíncrona. Diferente de promessas em outras linguagens, um Future em Rust é preguiçoso: ele só progride quando explicitamente "pollado" (consultado).
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
1.3. Por que Rust escolheu async/await em vez de green threads
Linguagens como Go e Erlang adotaram green threads (goroutines/processos leves) que são gerenciadas pelo runtime. Rust optou por async/await por duas razões principais:
- Zero-cost abstraction: async/await não impõe overhead em tempo de execução se você não o utilizar
- Sem runtime obrigatório: você pode escolher entre Tokio, async-std, smol ou até implementar seu próprio executor mínimo
2. A Anatomia de um Future
2.1. O trait Future
O coração do sistema assíncrono é o trait Future. Ele possui um único método poll que recebe:
- Pin<&mut Self>: uma referência pinada para o próprio future
- &mut Context: contém o Waker que notificará o executor quando o future puder progredir
2.2. Estados internos: Poll::Pending e Poll::Ready
O método poll retorna um dos dois estados:
- Poll::Ready(valor): o future foi concluído e produziu um valor
- Poll::Pending: o future ainda não está pronto; o executor deve chamar poll novamente quando notificado
use std::task::Poll;
fn exemplo_poll() -> Poll<u32> {
// Simula uma operação que ainda não terminou
if condicao_nao_atendida() {
Poll::Pending
} else {
Poll::Ready(42)
}
}
2.3. Máquina de estados gerada pelo compilador
Quando você escreve uma async fn, o compilador Rust a transforma em uma máquina de estados. Cada ponto de .await se torna um estado onde o future pode ser suspenso e retomado.
async fn exemplo_async() -> u32 {
let a = operacao_lenta_1().await; // Estado 1
let b = operacao_lenta_2().await; // Estado 2
a + b // Estado 3 (final)
}
3. A Sintaxe async e await
3.1. Criando futures
Você pode criar futures de duas formas principais:
// Função assíncrona
async fn buscar_dados() -> String {
String::from("dados carregados")
}
// Bloco async
let future = async {
let resultado = buscar_dados().await;
format!("Resultado: {}", resultado)
};
3.2. O operador .await
O operador .await é o ponto onde o future atual pode ser suspenso. Ele delega o polling ao executor e retorna o valor quando o future interno é concluído.
async fn processar() -> u32 {
// O compilador insere um loop de polling aqui
let valor = outro_future().await;
valor * 2
}
3.3. Composição com join! e select!
Para executar múltiplos futures concorrentemente, usamos macros:
use futures::future::{join, select};
use futures::pin_mut;
async fn tarefa_a() -> u32 { 100 }
async fn tarefa_b() -> u32 { 200 }
async fn exemplo_join() {
// Executa ambos concorrentemente e aguarda ambos
let (a, b) = join!(tarefa_a(), tarefa_b());
println!("Soma: {}", a + b);
}
async fn exemplo_select() {
let future_a = tarefa_a();
let future_b = tarefa_b();
pin_mut!(future_a, future_b);
// Retorna quando o primeiro future completar
match select(future_a, future_b).await {
Either::Left((valor_a, _)) => println!("A venceu: {}", valor_a),
Either::Right((valor_b, _)) => println!("B venceu: {}", valor_b),
}
}
4. Executores (Executors) e Reatores (Reactors)
4.1. O executor como driver de polling
Um executor é responsável por chamar poll repetidamente nos futures até que eles completem. O executor mais simples é block_on da crate futures:
use futures::executor::block_on;
fn main() {
let resultado = block_on(async {
let x = async { 42 }.await;
x * 2
});
println!("Resultado: {}", resultado); // 84
}
4.2. O reator como notificador de eventos
Enquanto o executor faz o polling, o reator gerencia eventos de I/O, timers e canais. Quando um recurso fica disponível, o reator chama o Waker associado ao future, sinalizando ao executor que ele deve tentar poll novamente.
4.3. Diferença entre runtime (Tokio) e executor mínimo
Um runtime completo como Tokio inclui:
- Executor multi-threaded
- Reator para I/O assíncrono (epoll, kqueue, IOCP)
- Temporizadores
- Canais de comunicação
Já um executor mínimo como futures::executor é single-threaded e não possui suporte a I/O real.
5. Pinning e Auto-referências
5.1. O problema das auto-referências
Máquinas de estado geradas por async fn podem conter auto-referências. Por exemplo, um future que armazena um buffer e uma referência a ele:
async fn problema() {
let buffer = vec![1, 2, 3];
let referencia = &buffer; // Auto-referência!
usar(buffer, referencia).await;
}
Se o future for movido na memória, a referência se torna inválida.
5.2. Pin<P> e Unpin
Pin<P> garante que o valor apontado não será movido. Tipos que implementam Unpin podem ser movidos mesmo quando pinados:
use std::pin::Pin;
// A maioria dos tipos implementa Unpin automaticamente
let mut numero = 42;
let pinado = Pin::new(&mut numero); // Ok, i32 é Unpin
// Futures async não implementam Unpin
async fn nao_unpin() -> i32 { 42 }
let future = nao_unpin();
// Pin::new(&mut future); // Erro! Future não implementa Unpin
5.3. Box::pin vs. pin_mut!
Para lidar com futures não-Unpin, temos duas opções:
use futures::pin_mut;
async fn exemplo() -> i32 { 42 }
// Alocação na heap
let future_heap = Box::pin(exemplo());
let resultado = block_on(future_heap);
// Alocação na stack (mais eficiente)
let future_stack = exemplo();
pin_mut!(future_stack);
let resultado = block_on(future_stack);
6. Cancelamento Seguro e Drop
6.1. Como o cancelamento ocorre
Quando um future é descartado (drop) sem ser completamente .awaitado, ele é cancelado. O drop do future executa os destrutores de todos os recursos parciais que foram alocados.
async fn recurso_temporario() {
let arquivo = abrir_arquivo().await; // Recurso alocado
// Se este future for cancelado aqui, o arquivo é fechado automaticamente
processar(arquivo).await;
}
6.2. Limpeza de recursos
O trait Drop é implementado automaticamente para futures, garantindo que todos os recursos sejam liberados no cancelamento:
struct RecursoImportante {
id: u32,
}
impl Drop for RecursoImportante {
fn drop(&mut self) {
println!("Recurso {} liberado", self.id);
}
}
async fn usar_recurso() {
let recurso = RecursoImportante { id: 1 };
// Se cancelado aqui, drop é chamado
trabalhar(recurso).await;
}
6.3. select! e cancelamento condicional
A macro select! cancela o future perdedor automaticamente:
use tokio::select;
use tokio::time::{sleep, Duration};
async fn exemplo_cancelamento() {
let operacao = async {
sleep(Duration::from_secs(10)).await;
"completa"
};
let timeout = sleep(Duration::from_secs(1));
select! {
resultado = operacao => println!("Operação {}", resultado),
_ = timeout => println!("Timeout! Operação cancelada"),
}
// O future da operação é automaticamente dropado
}
7. Implementando Futures Customizados
7.1. Criando um tipo que implementa Future
Vamos implementar um future que aguarda um timeout simples:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
struct Timeout {
deadline: Instant,
iniciado: bool,
}
impl Timeout {
fn new(duracao: Duration) -> Self {
Timeout {
deadline: Instant::now() + duracao,
iniciado: false,
}
}
}
impl Future for Timeout {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
if !this.iniciado {
this.iniciado = true;
// Agenda o waker para ser chamado no deadline
let deadline = this.deadline;
std::thread::spawn(move || {
std::thread::sleep(deadline - Instant::now());
cx.waker().wake_by_ref();
});
}
if Instant::now() >= this.deadline {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
7.2. Gerenciamento de estado entre chamadas de poll
O estado deve ser preservado entre chamadas de poll. Usamos campos no struct para isso:
struct Contador {
valor: u32,
maximo: u32,
}
impl Future for Contador {
type Output = u32;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
this.valor += 1;
if this.valor >= this.maximo {
Poll::Ready(this.valor)
} else {
Poll::Pending
}
}
}
7.3. Exemplo prático: future de timeout completo
async fn usar_timeout_customizado() {
let timeout = Timeout::new(Duration::from_secs(2));
timeout.await;
println!("2 segundos se passaram!");
}
fn main() {
futures::executor::block_on(usar_timeout_customizado());
}
Conclusão
Async/await em Rust oferece um modelo de concorrência eficiente e de baixo custo, combinando a segurança de memória da linguagem com controle explícito sobre a execução. A escolha por futures explícitos em vez de green threads permite que o programador tenha controle total sobre alocação de recursos e comportamento em tempo de execução.
Referências
- The Rust Async Book — Guia oficial sobre programação assíncrona em Rust, cobrindo desde fundamentos até tópicos avançados
- Tokio Documentation - Async in Depth — Tutorial do Tokio explicando async/await, executors e práticas recomendadas
- Rust Reference - Async and await — Documentação oficial da sintaxe async/await na referência da linguagem Rust
- Futures-rs crate documentation — Documentação da crate futures, incluindo join!, select! e pin_mut!
- Jon Gjengset's "Async Rust" YouTube series — Série de vídeos aprofundando conceitos de async/await, pinning e implementação de futures
- Rustnomicon - Pin — Explicação detalhada sobre Pin, Unpin e garantias de estabilidade de memória
- Async Rust: What is an Executor? — Artigo técnico explicando o papel dos executors no ecossistema async Rust