O Modelo Async do Rust
Async/await em Rust se parece com async/await em JavaScript ou C#, mas o mecanismo subjacente é fundamentalmente diferente, e essa diferença explica a maior parte do comportamento surpreendente que os novatos encontram: futures que "não fazem nada" até serem aguardados (awaited), um erro de compilação mencionando Pin, ou confusão sobre por que async fn main precisa de um macro de atributo. O modelo async do Rust é construído a partir de um único trait, Future, e uma escolha deliberada de não enviar um runtime na biblioteca padrão.
Esta página é a âncora conceitual para o restante da seção async-futures. As outras páginas aqui cobrem mecânicas específicas: o método poll do trait Future, executores, pinning, streams e cancelamento. Esta página as une em um único modelo mental: futures como máquinas de estados preguiçosas, executores como o que as impulsiona, e pinning como uma consequência estrutural de como o compilador constrói essas máquinas de estados.
Resumo
async fne blocosasynccompilam para tipos anônimos que implementam o traitFuture, uma máquina de estados que progride apenas quando algo chamapollnela.- Por que Importa: Este design permite que o Rust execute um grande número de operações concorrentes em um pequeno pool de threads com zero alocação de heap por tarefa por padrão, ao custo de precisar de um runtime externo para realmente executar qualquer coisa.
- Conceitos Chave:
Future,poll, executor, waker, máquina de estados,Pin. - Quando Usar: Entender este modelo é importante sempre que você depurar uma tarefa travada, escolher um runtime, escrever um
Futurepersonalizado ou encontrar um erro de compiladorSend/Pinque você não reconhece imediatamente. - Limitações / Trade-offs: O design preguiçoso e sem runtime empurra a complexidade real, o agendamento, a prontidão de I/O, os timers para crates de terceiros, então "Rust async" sempre significa realmente "Rust async mais um runtime".
- Tópicos Relacionados: o contrato
polldo traitFuture, executores e runtimes, pinning eUnpin, agendamento cooperativo de tarefas.
Fundamentos
O fato mais importante sobre futures do Rust é que eles são preguiçosos. Criar um valor com um bloco async ou chamar uma async fn não faz nenhum trabalho; apenas constrói um valor. O trabalho só acontece quando esse valor é chamado (polled), e chamar (polling) não é algo que o código da aplicação normalmente faz diretamente; um executor faz isso.
Esta é uma departure deliberada das promises do JavaScript, que começam a executar no momento em que são criadas. Um future do Rust é mais próximo de um iterador não iniciado: let fut = fetch_data(); aloca uma máquina de estados e não faz mais nada, exatamente como let iter = vec.iter(); produz um iterador que ainda não tocou em nenhum elemento.
Cada async fn e bloco async é açúcar sintático. O compilador transforma o corpo da função em uma struct anônima que implementa o trait Future, com as variáveis locais do corpo se tornando os campos da struct e cada ponto de .await se tornando um possível estado de pausa. Conceitualmente:
// async fn fetch() -> u32 { let x = step_one().await; step_two(x).await }
// compila para algo como um enum:
enum FetchState {
Start,
WaitingOnStepOne { /* captured locals */ },
WaitingOnStepTwo { x: u32 },
Done,
}Esta máquina de estados tem um método que importa: poll, que retorna Poll::Ready(value) quando o trabalho está completo, ou Poll::Pending quando precisa esperar por algo, um byte de rede de entrada, um timer, outro future. async/await é apenas uma maneira conveniente de escrever essa máquina de estados manualmente sem realmente escrever um enum e uma instrução match você mesmo.
Mecânicas e Interações
Um Future não pode se fazer rodar mais rápido por desejo; ele precisa de algo externo a ele chamando poll repetidamente até que retorne Poll::Ready. Esse algo é um executor, e a biblioteca padrão do Rust deliberadamente não fornece um.
Essa divisão é intencional e espelha um padrão mais amplo na linguagem: a biblioteca padrão define o trait (o contrato), e o ecossistema fornece implementações ajustadas para diferentes ambientes. Tokio, async-std, smol e runtimes embarcados como Embassy implementam o mesmo contrato Future de maneiras diferentes, um otimizado para servidores com pools de threads, outro para microcontroladores sem heap. Uma biblioteca que usa apenas async fn e nunca spawna um runtime por si só pode rodar sob qualquer um deles.
Chamar poll por si só seria desperdício se o executor apenas chamasse poll em um loop apertado, então um segundo mecanismo completa o quadro: o waker. Quando um future retorna Poll::Pending, ele recebe um Waker como parte do contexto de polling, e é responsável por chamar esse waker mais tarde, tipicamente de um callback de conclusão de I/O ou um timer, para dizer ao executor "me chame (poll) novamente, eu posso estar pronto agora". Essa passagem de waker permite que um executor estacione uma tarefa eficientemente em vez de ficar rodando em loop, e é o mesmo mecanismo se o future está esperando por um socket TCP ou um timer de cinco segundos.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct Ready(bool);
impl Future for Ready {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.0 { Poll::Ready(()) } else {
cx.waker().wake_by_ref(); // pede para ser chamado (polled) novamente
Poll::Pending
}
}
}A assinatura poll acima esconde um detalhe que confunde quase todo mundo na primeira vez que a vê: self: Pin<&mut Self> em vez do &mut self comum. É aqui que o pinning entra em cena, e é uma consequência estrutural direta da transformação da máquina de estados descrita em Fundamentos.
Considere um bloco async que empresta (borrows) uma de suas próprias variáveis locais através de um .await, algo como let buf = [0u8; 64]; let n = reader.read(&mut buf).await; seguido por código posterior que lê buf novamente. A máquina de estados gerada pelo compilador precisa armazenar tanto buf quanto uma referência a buf como campos da mesma struct, o que torna a struct auto-referencial: um campo aponta para dentro de outro campo do mesmo valor. Valores Rust comuns podem ser movidos livremente (um memcpy nos bastidores), mas mover uma struct auto-referencial invalida o ponteiro interno, pois ele passaria a apontar para a antiga localização de memória em vez da nova. Pin<P> é um wrapper de ponteiro que promete ao compilador "o valor por trás deste ponteiro nunca se moverá novamente", que é exatamente a garantia que uma máquina de estados auto-referencial precisa para ser segura (sound).
Considerações Avançadas e Aplicações
A maior parte do código da aplicação nunca escreve Pin explicitamente, porque .await faz o pinning de futures na pilha automaticamente, e isso é por design: pinning é um mecanismo que a linguagem precisa, não uma API com a qual a maioria dos usuários deve interagir diretamente. Ele aparece em três situações recorrentes: implementar um Future manualmente, armazenar um future dentro de um campo de struct (o que requer Pin<Box<dyn Future<...>>> já que um campo de struct não pode ser fixado (pinned) na pilha da mesma forma que uma variável local), e ler um erro de compilador !Unpin, que geralmente significa que um future foi movido depois que algo já começou a chamá-lo (polling).
O auto trait Unpin é a válvula de escape que impede que o pinning seja doloroso para o caso comum. Quase todos os tipos Rust comuns são Unpin, o que significa que movê-los após o pinning é aceitável, pois eles não contêm auto-referências. Apenas máquinas de estados async geradas, e tipos auto-referenciais escritos manualmente, são tipicamente !Unpin, e apenas esses realmente precisam da disciplina que Pin impõe.
Runtimes constroem modelos de agendamento e I/O significativamente diferentes sobre o mesmo contrato Future, e a escolha tem consequências operacionais reais, não apenas diferenças de API.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Executor multi-threaded de roubo de trabalho (padrão Tokio) | Escala entre núcleos; bom padrão para servidores | Requer que futures spawnados sejam Send; mais partes móveis para raciocinar | Serviços de rede de produção |
Executor single-threaded (current_thread, LocalSet) | Modelo mais simples; suporta futures !Send | Sem paralelismo; uma tarefa lenta bloqueia tudo mais naquele thread | Testes, embarcados, código com muitas !Send |
Futures alocados no heap com Box::pin | Permite armazenar/retornar futures uniformemente, incluindo objetos de trait | Uma alocação por future em caixa; custo de indireção leve | Despacho dinâmico, futures armazenados em coleções |
Pinning na pilha (macro pin!) | Zero alocação extra | Só funciona para locais que não precisam ser retornados ou armazenados | Uso local apenas de .await dentro de uma função |
O design preguiçoso e baseado em poll tem consequências diretas para o cancelamento, o que vale a pena nomear explicitamente porque difere do cancelamento baseado em threads. Soltar (dropping) um future em qualquer ponto de await simplesmente para de chamá-lo (polling), não há sinal entregue para "interromper" o trabalho em andamento da maneira que uma thread pode receber um sinal. Isso torna o cancelamento barato e estrutural (solte o future, sua implementação Drop é executada, recursos são liberados), mas também significa que a segurança de cancelamento, se uma operação parcialmente concluída deixa os dados em um estado consistente, é algo que o autor do future tem que projetar explicitamente em vez de ser garantido automaticamente pelo runtime.
Equívocos Comuns
- "Chamar uma função async inicia o trabalho imediatamente." Ela constrói um valor
Futurepreguiçoso e não faz mais nada; o trabalho começa apenas quando algo a chama (polls), tipicamente aguardando-a (awaiting) ou spawnando-a em um executor. - "A biblioteca padrão do Rust inclui um runtime async." Ela define o trait
Futuree o contrato de polling, mas não envia nenhum executor; todo programa async real depende de um crate de runtime como Tokio. - "
Pinsignifica a mesma coisa que imutável."Pinapenas impede que o endereço de memória de um valor mude; o valor ainda pode ser mutado no local através dePin::as_mutou métodos seguros que não o movem. - "Você sempre precisa pensar sobre Pin ao escrever código async."
.awaitfaz o pinning de futures na pilha automaticamente para você;Pingeralmente só aparece ao implementarFuturemanualmente ou ao armazenar um future dentro de outra struct. - "async/await é um recurso especial de runtime, não código 'real'." É um açúcar sintático comum sobre uma máquina de estados implementando um trait comum; não há mágica oculta de runtime além do próprio protocolo
poll/Waker. - "Soltar (
dropping) um future o cancela da mesma forma que matar uma thread." Soltar simplesmente para de chamar (polling) e executaDrop; não há sinal de interrupção, então quaisquer efeitos colaterais parciais já realizados antes do ponto de soltura não são automaticamente desfeitos.
FAQs
O que significa que os futures do Rust são "preguiçosos"?
Criar um future, seja chamando uma async fn ou escrevendo um bloco async, não faz trabalho por si só. Ele apenas produz um valor que implementa Future; a execução acontece depois, quando um executor chama poll nesse valor.
Por que um programa async precisa de um runtime externo como Tokio?
A biblioteca padrão define o trait Future, o contrato para "uma unidade de trabalho que pode ser chamada (polled)", mas deliberadamente não inclui um scheduler, um reator de I/O ou timers. Esses são fornecidos por crates de runtime para que diferentes ambientes (servidores, dispositivos embarcados, testes) possam escolher uma implementação que se encaixe.
O que realmente acontece quando escrevo `.await`?
Ele descompila para um loop que chama poll no future, e se ele retornar Poll::Pending, suspende a máquina de estados envolvente naquele ponto e devolve o controle para quem está impulsionando o future externo, geralmente o executor.
O que é um waker e por que o polling precisa de um?
Um Waker é um handle que um future pendente armazena e mais tarde invoca (frequentemente de um callback de conclusão de I/O) para dizer ao executor "me chame (poll) novamente". Sem ele, um executor teria que chamar poll em cada future pendente repetidamente em um loop ocupado em vez de dormir até que haja progresso real a ser feito.
Por que poll recebe `Pin<&mut Self>` em vez de `&mut self`?
Porque a máquina de estados gerada pelo compilador por trás de um bloco async pode ser auto-referencial, mantendo uma referência dentro de um de seus próprios campos através de um ponto de await. Pin garante que o valor não se moverá na memória após ser fixado (pinned), o que é o que mantém essa referência interna válida.
Preciso usar Pin no código de aplicação async do dia a dia?
Raramente. .await faz o pinning de futures na pilha automaticamente para você; Pin geralmente só aparece ao implementar Future manualmente ou ao armazenar um future dentro de outra struct.
Qual a diferença entre Unpin e !Unpin?
Tipos Unpin podem ser movidos com segurança mesmo após serem fixados (pinned), porque não contêm auto-referências; quase todos os tipos Rust comuns são Unpin. Tipos !Unpin, principalmente máquinas de estados async geradas pelo compilador, devem permanecer em um endereço de memória fixo após serem fixados (pinned).
Por que eu escolheria um runtime single-threaded em vez de um multi-threaded?
Um executor single-threaded suporta futures que são !Send, e remove a sobrecarga e a complexidade do agendamento entre threads. Ele não tem paralelismo, então se encaixa em testes, alvos embarcados e código que depende especificamente de estado thread-local ou não thread-safe.
O que acontece se eu soltar (`drop`) um future no meio de um await?
O polling simplesmente para, e a implementação Drop do future é executada como qualquer outro valor saindo de escopo. Não há interrupção entregue no meio da operação; quaisquer efeitos colaterais já confirmados antes do ponto de soltura não são automaticamente desfeitos.
async/await é apenas açúcar sintático, ou muda como o código roda?
É açúcar sintático sobre o mesmo modelo Future/poll/Waker que você usaria escrevendo uma implementação manual. A máquina de estados gerada se comporta identicamente a uma escrita manualmente, apenas sem o código repetitivo (boilerplate).
Por que alguns crates usam `Box::pin` em vez da macro `pin!`?
Box::pin aloca o future no heap, o que permite que ele seja retornado de uma função, armazenado em um campo de struct, ou usado como um objeto de trait dyn Future. A macro pin! faz o pinning na pilha sem alocação, mas o resultado não pode sobreviver ao escopo da função atual da mesma forma.
Como o modelo async do Rust difere das promises do JavaScript?
Uma promise JavaScript começa a executar assim que é criada; um future Rust não faz nada até ser chamado (polled). Isso torna os futures Rust mais próximos de iteradores não iniciados do que de promises, e é por isso que um future Rust não aguardado (unawaited) e não spawnado silenciosamente não faz nada.
Relacionados
- O Trait Future - o contrato
pollem detalhes completos, com um exemplo deFutureescrito manualmente. - Executors & Runtimes - o que impulsiona o polling, e como os sabores do Tokio diferem.
- Pinning & Unpin - a API
Pine futures auto-referenciais em profundidade. - Noções Básicas de Async - exemplos práticos de
async/awaitbaseados neste modelo. - Streams - o equivalente async de um iterador, construído sobre o mesmo modelo de polling.
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.