Pular para o conteúdo

Async/Await em Rust

Imagine um restaurante. Na página de Concorrência, você aprendeu sobre threads. Isso é como contratar um garçom por mesa. Funciona, mas garçons custam caro, e na maior parte do tempo eles só ficam parados esperando a cozinha.

Async é como ter um garçom que atende vários pedidos. Ele anota o pedido da mesa 1, leva para a cozinha e, enquanto a comida fica pronta, vai até a mesa 2. Ninguém fica parado à toa. Essa é a ideia por trás de async e .await em Rust: enquanto uma tarefa está esperando (pela rede, por um arquivo, por um timer), outra tarefa ganha a vez de rodar.

O async brilha quando o seu programa passa muito tempo esperando: servidores web, clientes de API, apps de chat, bots. Esta página usa o tokio, o runtime async mais popular do Rust.

A biblioteca padrão do Rust traz as palavras-chave async e .await, mas não traz o “motor” que realmente executa o código async. Esse motor se chama runtime (ambiente de execução), e o tokio é o que a maioria dos projetos usa. Adicione-o a um projeto novo:

Terminal window
cargo new async_demo
cd async_demo
cargo add tokio --features full

Isso adiciona esta linha ao seu Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

A feature full liga tudo (timers, tarefas, rede e mais). É a escolha mais fácil enquanto você aprende. Os botões “Experimente você mesmo” desta página também funcionam: o Rust Playground já vem com o tokio.

use std::time::Duration;
async fn make_coffee() -> String {
tokio::time::sleep(Duration::from_millis(20)).await;
String::from("café")
}
#[tokio::main]
async fn main() {
println!("Fazendo o pedido...");
let drink = make_coffee().await;
println!("Aqui está o seu {}", drink);
}

Saída:

Fazendo o pedido...
Aqui está o seu café
▶ Experimente você mesmo

Três coisas novas aqui:

  • async fn cria uma função que pode pausar no meio e continuar depois.
  • .await quer dizer “espere isto terminar e, enquanto espera, deixe outras tarefas rodarem”. Você só pode usar .await dentro de uma função async.
  • #[tokio::main] inicia o runtime do tokio e executa o seu async fn main nele. Sem isso, o main não pode ser async.

tokio::time::sleep é a versão async de “espere um pouco”. Ele pausa só esta tarefa, não a thread inteira.

Esta é a parte que mais surpreende as pessoas. Chamar uma async fn não a executa. Ela te devolve uma future (algo como “valor futuro”): um valor que significa “um trabalho que vai acontecer depois”. Nada acontece até você usar .await nela:

async fn say_hello() {
println!("Olá de dentro da future!");
}
#[tokio::main]
async fn main() {
let future = say_hello(); // nada roda ainda
println!("Future criada, mas não iniciada");
future.await; // agora ela roda
println!("Pronto");
}

Saída:

Future criada, mas não iniciada
Olá de dentro da future!
Pronto
▶ Experimente você mesmo

Pense em uma future como uma comanda escrita. Escrever a comanda não prepara a comida. Entregá-la na cozinha (.await) é que prepara.

Até agora, esperamos uma coisa de cada vez. O poder de verdade vem de esperar várias coisas ao mesmo tempo. tokio::join! executa várias futures juntas e espera até todas terminarem. Ele te devolve os resultados em uma tupla:

use std::time::Duration;
use tokio::time::sleep;
async fn fetch_user() -> String {
sleep(Duration::from_millis(30)).await; // finja que isto é uma chamada de rede
String::from("Ana")
}
async fn fetch_order_count() -> u32 {
sleep(Duration::from_millis(20)).await;
3
}
#[tokio::main]
async fn main() {
let (user, orders) = tokio::join!(fetch_user(), fetch_order_count());
println!("{} tem {} pedidos", user, orders);
}

Saída:

Ana tem 3 pedidos
▶ Experimente você mesmo

Se você esperasse uma depois da outra, a espera total seria de uns 30 + 20 = 50 ms. Com join!, as duas esperas acontecem ao mesmo tempo, então leva uns 30 ms: o tempo da mais lenta. Com chamadas de rede de verdade, essa diferença cresce rápido.

O join! é ótimo quando você tem um número fixo de futures. Quando você tem uma lista de trabalhos, use tokio::spawn. Ele entrega uma future ao runtime como uma tarefa (task) separada, começa a executá-la na hora e te devolve um JoinHandle. Depois, você usa .await no handle para pegar o resultado:

use std::time::Duration;
use tokio::time::sleep;
async fn download(id: u64) -> String {
sleep(Duration::from_millis(50 - id * 10)).await;
format!("arquivo {} baixado", id)
}
#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for id in 1..=3 {
let handle = tokio::spawn(download(id)); // começa a rodar agora
handles.push(handle);
}
for handle in handles {
let result = handle.await.unwrap();
println!("{}", result);
}
}

Saída:

arquivo 1 baixado
arquivo 2 baixado
arquivo 3 baixado
▶ Experimente você mesmo

Repare bem: o arquivo 3 tem o sleep mais curto, então na verdade ele termina primeiro. Mas imprimimos os resultados na ordem em que esperamos os handles, então a saída é sempre 1, 2, 3. Mesmo assim, os três downloads rodaram ao mesmo tempo.

E o .unwrap()? Esperar um JoinHandle dá um Result, porque a tarefa pode ter entrado em panic. Se isso acontecer, você recebe um Err em vez de derrubar o programa inteiro. Isso se parece bastante com thread::spawn e .join() da página de Concorrência, e não é por acaso.

Esquecer o .await. Como as futures são preguiçosas, esquecer o .await significa que o trabalho nunca acontece. Se você usar o valor, o compilador te impede:

async fn get_number() -> u32 {
5
}
#[tokio::main]
async fn main() {
let n = get_number(); // esqueceu o .await
println!("{}", n + 1);
}
error[E0369]: cannot add `{integer}` to `impl Future<Output = u32>`
--> src/main.rs:8:22
|
8 | println!("{}", n + 1);
| - ^ - {integer}
| |
| impl Future<Output = u32>

A mensagem diz que não dá para somar um inteiro a um impl Future<Output = u32>: n é uma future de um u32, não um u32. A correção é let n = get_number().await;. Se a função não retorna nada e você só a chama, como em save_file();, você recebe apenas um aviso (warning). Por isso, leia os seus avisos:

warning: unused implementer of `Future` that must be used
|
7 | save_file();
| ^^^^^^^^^^^
|
= note: futures do nothing unless you `.await` or poll them

O aviso lembra que futures não fazem nada a menos que você use .await nelas.

Usar std::thread::sleep dentro de código async. Este compila sem problemas, e é isso que o torna traiçoeiro:

async fn slow_task() {
std::thread::sleep(std::time::Duration::from_secs(1)); // trava a thread inteira!
}

std::thread::sleep congela a thread inteira. O runtime não consegue trocar para outras tarefas, então tudo naquela thread para: é como um garçom que pega no sono em pé ao lado de uma mesa. Dentro de código async, use sempre tokio::time::sleep(...).await. O mesmo vale para outros trabalhos lentos que bloqueiam, como ler arquivos enormes com std::fs: o tokio tem versões async (tokio::fs), ou você pode usar tokio::task::spawn_blocking.

Esperar que o async deixe o trabalho de CPU mais rápido. O async ajuda quando as tarefas estão esperando. Ele não ajuda quando as tarefas estão ocupadas, como redimensionar imagens ou fazer contas pesadas. Um garçom não cozinha mais rápido só porque anota mais pedidos. Para trabalho pesado de CPU, use threads (ou um crate como o rayon).

Use async quando… Use threads quando…
A maior parte do tempo é gasta esperando (rede, banco de dados, timers) O trabalho é de cálculo pesado
Você precisa de milhares de tarefas ao mesmo tempo (como em um servidor web) Você só precisa de algumas coisas rodando em paralelo
Você usa crates async como reqwest ou axum Você quer evitar adicionar um runtime como dependência

Você não precisa escolher só um. Muitos programas reais usam async para a parte de rede e threads para o trabalho pesado.

Precisa que as tarefas troquem mensagens entre si? O tokio tem o seu próprio canal, o tokio::sync::mpsc. Ele funciona como o da biblioteca padrão, que você viu na página de Canais (channels), só que você escreve rx.recv().await, então esperar por uma mensagem não bloqueia a thread.

A maior parte do ecossistema async do Rust é construída em cima do tokio. Dois crates que você vai ver em todo lugar:

  • reqwest: um cliente HTTP para chamar APIs web. Você escreve reqwest::get(url).await para buscar uma página.
  • axum: um framework web para construir servidores e APIs, feito pelo time do tokio.

Os dois combinam muito bem com o serde_json para ler e escrever JSON.

Escreva três funções async: make_toast (dorme 30ms), fry_eggs (dorme 20ms) e brew_coffee (dorme 10ms). Cada uma retorna uma String com o nome da comida. Rode as três ao mesmo tempo e imprima o que ficou pronto. Dica: o tokio::join! recebe várias futures e devolve uma tupla com os resultados delas.

Ver solução
use std::time::Duration;
use tokio::time::sleep;
async fn make_toast() -> String {
sleep(Duration::from_millis(30)).await;
String::from("torrada")
}
async fn fry_eggs() -> String {
sleep(Duration::from_millis(20)).await;
String::from("ovos")
}
async fn brew_coffee() -> String {
sleep(Duration::from_millis(10)).await;
String::from("café")
}
#[tokio::main]
async fn main() {
let (toast, eggs, coffee) = tokio::join!(make_toast(), fry_eggs(), brew_coffee());
println!("Café da manhã: {}, {} e {}", toast, eggs, coffee);
}

Saída:

Café da manhã: torrada, ovos e café
▶ Experimente você mesmo

O café fica pronto primeiro, mas a tupla sempre volta na ordem em que você listou as futures. O café da manhã inteiro leva uns 30ms (o tempo do prato mais lento), e não 60ms.

Escreva async fn square_later(n: u64) -> u64, que dorme 10 * n milissegundos e depois retorna n * n. No main, crie uma tarefa para cada número de 1 a 4, depois espere por todas e imprima o total. Dica: guarde cada handle de tokio::spawn(...) em um Vec, depois percorra os handles somando handle.await.unwrap().

Ver solução
use std::time::Duration;
use tokio::time::sleep;
async fn square_later(n: u64) -> u64 {
sleep(Duration::from_millis(10 * n)).await;
n * n
}
#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for n in 1..=4 {
handles.push(tokio::spawn(square_later(n)));
}
let mut total = 0;
for handle in handles {
total += handle.await.unwrap();
}
println!("total = {total}");
}

Saída:

total = 30
▶ Experimente você mesmo

As quatro tarefas começam na hora e dormem ao mesmo tempo, então isso leva uns 40ms, e não 10 + 20 + 30 + 40 = 100ms.

  • Async permite que uma thread cuide de muitas tarefas, alternando entre elas enquanto elas esperam.
  • O Rust precisa de um runtime para executar código async. Adicione o tokio com cargo add tokio --features full e marque o main com #[tokio::main].
  • Uma async fn retorna uma future. Futures são preguiçosas: nada roda até você usar .await.
  • Use tokio::join! para esperar várias futures ao mesmo tempo e tokio::spawn para iniciar tarefas e receber um JoinHandle.
  • Nunca use std::thread::sleep em código async; use tokio::time::sleep(...).await.
  • Async serve para esperar, não para trabalho pesado de CPU. Para isso, use threads.

Teste o que aprendeu

1. O que acontece quando você chama uma async fn sem .await?

2. O que #[tokio::main] faz?

3. Duas futures dormem por 30 ms e 20 ms. Quanto tempo, mais ou menos, leva um tokio::join! com as duas?

4. Por que std::thread::sleep é um problema dentro de código async?

5. Para qual trabalho o async é mais indicado?

Pontuação: 0 / 5