No artigo anterior exploramos concorrência com threads do sistema operacional — cada thread tem sua própria stack, é gerenciada pelo OS, e tem custo real de memória e tempo de criação. Para muitos problemas, especialmente aqueles dominados por I/O — chamadas de rede, leitura de banco de dados, leitura de arquivos — threads são uma ferramenta pesada demais.
Imagine um servidor web que precisa lidar com 100.000 conexões simultâneas. Criar 100.000 threads seria catastrófico — cada thread consome pelo menos 8KB de stack, resultando em gigabytes de memória só para as stacks. A solução é async/await: concorrência cooperativa que permite lidar com milhares de operações simultâneas usando apenas algumas threads.
O problema que async resolve
Em código síncrono, quando você lê um arquivo ou faz uma chamada de rede, a thread bloqueia — fica parada esperando a resposta. Nesse tempo, a thread não pode fazer nada útil.
Código assíncrono resolve isso: quando uma operação estaria para bloquear, a tarefa cede o controle para um executor, que pode executar outras tarefas enquanto espera. Quando a operação completa, a tarefa original é retomada.
Em Rust, isso é implementado através de Future — um valor que representa uma computação que pode não ter completado ainda. O executor gerencia um pool de tarefas, cada uma sendo uma Future.
Configurando o ambiente
Rust não inclui um executor async na biblioteca padrão — apenas a infraestrutura (Future, async, await). O executor mais popular é o Tokio:
# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
O mínimo para usar Tokio é decorar o main com #[tokio::main]:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Iniciando...");
sleep(Duration::from_millis(100)).await;
println!("Após 100ms");
}
O #[tokio::main] transforma o main assíncrono em código síncrono que cria o runtime do Tokio e executa a future. Internamente, expande para algo como:
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async_main());
}
async e await — a sintaxe
Funções assíncronas são declaradas com async fn. Elas retornam uma Future implicitamente. Para obter o valor de uma Future, use .await:
use tokio::time::{sleep, Duration};
async fn buscar_dados(id: u32) -> String {
println!("Buscando dados do id {id}...");
sleep(Duration::from_millis(100)).await; // simula I/O
format!("Dados do registro {id}")
}
async fn processar(id: u32) -> String {
let dados = buscar_dados(id).await; // aguarda a future completar
format!("Processado: {dados}")
}
#[tokio::main]
async fn main() {
let resultado = processar(42).await;
println!("{resultado}");
}
O .await é um ponto de suspensão — quando a tarefa encontra um .await, ela pode ceder o controle para o executor se a operação não estiver pronta. Quando a operação completa, a tarefa é retomada do ponto onde parou.
Concorrência real com tokio::join!
A grande vantagem de async aparece quando você executa múltiplas operações simultaneamente. Com join!, múltiplas futures executam concorrentemente:
use tokio::time::{sleep, Duration, Instant};
async fn tarefa(nome: &str, duracao_ms: u64) -> String {
println!(" Iniciando: {nome}");
sleep(Duration::from_millis(duracao_ms)).await;
println!(" Concluída: {nome}");
format!("Resultado de {nome}")
}
#[tokio::main]
async fn main() {
let inicio = Instant::now();
// Sequencial — demora a soma de todas as durações
println!("── Sequencial ──");
let r1 = tarefa("A", 100).await;
let r2 = tarefa("B", 150).await;
let r3 = tarefa("C", 80).await;
println!("Tempo: {:?}
", inicio.elapsed());
let inicio = Instant::now();
// Concorrente — demora a duração da mais longa
println!("── Concorrente ──");
let (r1, r2, r3) = tokio::join!(
tarefa("X", 100),
tarefa("Y", 150),
tarefa("Z", 80),
);
println!("Tempo: {:?}", inicio.elapsed());
println!("{r1}, {r2}, {r3}");
}
Saída:
── Sequencial ──
Iniciando: A
Concluída: A
Iniciando: B
Concluída: B
Iniciando: C
Concluída: C
Tempo: 330ms
── Concorrente ──
Iniciando: X
Iniciando: Y
Iniciando: Z
Concluída: Z
Concluída: X
Concluída: Y
Tempo: 150ms
No modo concorrente, as três tarefas executam simultaneamente — o tempo total é o da tarefa mais longa, não a soma de todas.
tokio::spawn — tarefas independentes
Para lançar tarefas que rodam independentemente (sem precisar aguardar todas juntas), use tokio::spawn:
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
async fn worker(id: u32) -> u32 {
sleep(Duration::from_millis(id as u64 * 50)).await;
println!("Worker {id} concluído");
id * id
}
#[tokio::main]
async fn main() {
let mut handles: Vec<JoinHandle<u32>> = Vec::new();
for id in 1..=5 {
handles.push(tokio::spawn(worker(id)));
}
let mut total = 0u32;
for handle in handles {
total += handle.await.unwrap();
}
println!("Soma dos quadrados: {total}"); // 55
}
tokio::spawn é análogo a thread::spawn — mas cria uma tarefa assíncrona leve em vez de uma thread do OS.
Canais assíncronos com Tokio
Tokio oferece canais otimizados para uso assíncrono:
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
async fn produtor(tx: mpsc::Sender<String>, id: u32) {
for i in 0..3 {
sleep(Duration::from_millis(50)).await;
let msg = format!("Produtor {id} — item {i}");
tx.send(msg).await.unwrap();
}
}
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32); // buffer de 32 mensagens
// Lança 3 produtores
for id in 1..=3 {
let tx_clone = tx.clone();
tokio::spawn(produtor(tx_clone, id));
}
// Necessário: descarta o tx original
drop(tx);
// Consome todas as mensagens
let mut total = 0;
while let Some(msg) = rx.recv().await {
println!("Recebido: {msg}");
total += 1;
}
println!("Total recebido: {total} mensagens"); // 9
}
Tratamento de erros em código async
O operador ? funciona normalmente em funções async:
use tokio::fs;
use std::io;
async fn ler_e_processar(caminho: &str) -> Result<usize, io::Error> {
let conteudo = fs::read_to_string(caminho).await?;
let linhas = conteudo.lines().count();
Ok(linhas)
}
async fn processar_multiplos(caminhos: &[&str]) -> Vec<Result<usize, io::Error>> {
let futures: Vec<_> = caminhos
.iter()
.map(|&caminho| ler_e_processar(caminho))
.collect();
// futures::future::join_all executa todas concorrentemente
// (requer crate futures)
let mut resultados = Vec::new();
for caminho in caminhos {
resultados.push(ler_e_processar(caminho).await);
}
resultados
}
#[tokio::main]
async fn main() {
match ler_e_processar("dados.txt").await {
Ok(linhas) => println!("Arquivo tem {linhas} linhas"),
Err(e) => println!("Erro: {e}"),
}
}
Um programa completo: cliente HTTP assíncrono
Vamos construir um cliente que faz múltiplas requisições HTTP em paralelo. Adicione ao Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use reqwest::Client;
use serde::Deserialize;
use std::time::Instant;
use tokio::time::{timeout, Duration};
#[derive(Debug, Deserialize)]
struct Post {
id: u32,
title: String,
body: String,
#[serde(rename = "userId")]
user_id: u32,
}
#[derive(Debug, Deserialize)]
struct Usuario {
id: u32,
name: String,
email: String,
}
#[derive(Debug)]
struct PostCompleto {
post: Post,
autor: Usuario,
}
async fn buscar_post(client: &Client, id: u32) -> Result<Post, reqwest::Error> {
let url = format!("https://jsonplaceholder.typicode.com/posts/{id}");
client.get(&url).send().await?.json::<Post>().await
}
async fn buscar_usuario(client: &Client, id: u32) -> Result<Usuario, reqwest::Error> {
let url = format!("https://jsonplaceholder.typicode.com/users/{id}");
client.get(&url).send().await?.json::<Usuario>().await
}
async fn buscar_post_completo(
client: &Client,
post_id: u32,
) -> Result<PostCompleto, String> {
let post = buscar_post(client, post_id)
.await
.map_err(|e| format!("Erro ao buscar post {post_id}: {e}"))?;
let autor = buscar_usuario(client, post.user_id)
.await
.map_err(|e| format!("Erro ao buscar usuário: {e}"))?;
Ok(PostCompleto { post, autor })
}
async fn buscar_paralelo(
client: &Client,
ids: &[u32],
) -> Vec<Result<PostCompleto, String>> {
let futures: Vec<_> = ids
.iter()
.map(|&id| {
let client = client.clone();
tokio::spawn(async move {
let resultado = timeout(
Duration::from_secs(5),
buscar_post_completo(&client, id),
)
.await;
match resultado {
Ok(r) => r,
Err(_) => Err(format!("Timeout ao buscar post {id}")),
}
})
})
.collect();
let mut resultados = Vec::new();
for future in futures {
match future.await {
Ok(resultado) => resultados.push(resultado),
Err(e) => resultados.push(Err(format!("Erro de join: {e}"))),
}
}
resultados
}
#[tokio::main]
async fn main() {
let client = Client::new();
let ids_para_buscar = vec![1, 2, 3, 4, 5];
println!("Buscando {} posts em paralelo...
", ids_para_buscar.len());
let inicio = Instant::now();
let resultados = buscar_paralelo(&client, &ids_para_buscar).await;
let tempo = inicio.elapsed();
let mut sucessos = 0;
let mut erros = 0;
for resultado in &resultados {
match resultado {
Ok(post_completo) => {
sucessos += 1;
println!("── Post #{} ──", post_completo.post.id);
println!(" Título : {}", post_completo.post.title);
println!(" Autor : {} <{}>",
post_completo.autor.name,
post_completo.autor.email);
let preview = post_completo.post.body
.chars()
.take(60)
.collect::<String>();
println!(" Preview: {preview}...
");
}
Err(e) => {
erros += 1;
println!("Erro: {e}");
}
}
}
println!("── Resumo ──");
println!("Sucessos : {sucessos}");
println!("Erros : {erros}");
println!("Tempo : {:?}", tempo);
println!("(Sequencial seria ~{}x mais lento)",
ids_para_buscar.len());
}
Saída:
Buscando 5 posts em paralelo...
── Post #1 ──
Título : sunt aut facere repellat provident occaecati...
Autor : Leanne Graham <Sincere@april.biz>
Preview: quia et suscipit suscipit recusandae...
── Post #2 ──
Título : qui est esse
Autor : Ervin Howell <Shanna@melissa.tv>
Preview: est rerum tempore vitae sequi...
[...]
── Resumo ──
Sucessos : 5
Erros : 0
Tempo : 312ms
(Sequencial seria ~5x mais lento)
select! — corrida entre futures
Às vezes você quer executar a primeira operação que completar entre várias:
use tokio::time::{sleep, Duration};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<String>(1);
// Simula chegada de mensagem após 200ms
tokio::spawn(async move {
sleep(Duration::from_millis(200)).await;
tx.send(String::from("mensagem chegou!")).await.unwrap();
});
let timeout = sleep(Duration::from_millis(500));
tokio::pin!(timeout);
loop {
tokio::select! {
msg = rx.recv() => {
match msg {
Some(m) => println!("Recebido: {m}"),
None => { println!("Canal fechado."); break; }
}
}
_ = &mut timeout => {
println!("Timeout! Nenhuma mensagem a tempo.");
break;
}
}
}
}
select! aguarda múltiplas futures e executa o braço da primeira que completar — as demais são canceladas.
Async vs threads — quando usar cada um
A escolha entre threads e async não é óbvia. Uma orientação prática:
Use threads quando o trabalho é CPU-bound — cálculos pesados, processamento de imagens, computação numérica. Threads aproveitam múltiplos núcleos de CPU diretamente. rayon é a ferramenta certa aqui.
Use async quando o trabalho é I/O-bound — chamadas de rede, banco de dados, leitura de arquivos. A thread não precisa esperar ativamente — ela pode fazer outras coisas. Tokio é a ferramenta certa aqui.
// CPU-bound: use rayon (threads reais)
use rayon::prelude::*;
let resultado: Vec<_> = dados.par_iter()
.map(|x| calculo_pesado(x))
.collect();
// I/O-bound: use tokio (async)
let resultado = tokio::join!(
buscar_banco_dados(id1),
fazer_requisicao_http(url),
ler_arquivo(caminho),
);
Você também pode combinar os dois — Tokio tem spawn_blocking para executar código CPU-bound dentro de um contexto async sem bloquear o executor:
use tokio::task;
async fn processar_arquivo_pesado(caminho: String) -> Vec<u8> {
task::spawn_blocking(move || {
// Código síncrono e pesado — roda em thread separada
std::fs::read(&caminho).unwrap()
})
.await
.unwrap()
}
O modelo mental de async/await
Entender async em Rust fica mais fácil com o modelo mental correto:
Uma Future é uma máquina de estado gerada pelo compilador. Cada ponto de .await é um estado possível. O executor chama poll() na future — se o trabalho está pronto, retorna Poll::Ready(valor); se não, retorna Poll::Pending e registra um waker para ser notificado quando estiver pronto.
Você raramente implementa Future manualmente — async/await gera tudo isso automaticamente. Mas saber que existe explica por que código async em Rust não tem overhead de runtime excessivo: são máquinas de estado estáticas, sem alocações de heap desnecessárias.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 17 — Async and Await — https://doc.rust-lang.org/book/ch17-00-async-await.html
- "Asynchronous Programming in Rust" — guia completo e gratuito — https://rust-lang.github.io/async-book/
- Tokio Tutorial — tutorial oficial do Tokio com exemplos práticos — https://tokio.rs/tokio/tutorial
- Tokio Documentation — https://docs.rs/tokio
reqwestcrate — cliente HTTP assíncrono — https://docs.rs/reqwest- "Pin and Suffering" — Without Boats — artigo sobre Pin, necessário para Futures avançadas — https://without.boats/blog/pin/
- Jon Gjengset — "Crust of Rust: async/await" — https://www.youtube.com/watch?v=ThjvMReOXYM