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:

  1. Zero-cost abstraction: async/await não impõe overhead em tempo de execução se você não o utilizar
  2. 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