Async/Await Básico em Rust

Aprenda async/await em Rust com tokio. Runtime assíncrono, async fn, .await, join!, select!, e exemplos completos de concorrência assíncrona.

Async/Await Básico em Rust

A programação assíncrona em Rust permite executar muitas tarefas de I/O concorrentemente sem o custo de criar threads do sistema operacional. O runtime tokio é o mais popular e robusto do ecossistema.

Dependências

Cargo.toml:

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

Primeira função assíncrona

Uma função async retorna uma Future que precisa ser executada por um runtime:

use std::time::Duration;
use tokio::time::sleep;

// Funções async retornam implicitamente uma Future
async fn saudacao(nome: &str) -> String {
    // .await pausa esta função até o sleep completar
    sleep(Duration::from_millis(100)).await;
    format!("Olá, {}!", nome)
}

async fn calcular(x: i32, y: i32) -> i32 {
    sleep(Duration::from_millis(50)).await;
    x + y
}

// #[tokio::main] cria o runtime e executa a função async
#[tokio::main]
async fn main() {
    println!("Início");

    let msg = saudacao("Rust").await;
    println!("{}", msg);

    let resultado = calcular(20, 22).await;
    println!("20 + 22 = {}", resultado);

    println!("Fim");
}

Saída:

Início
Olá, Rust!
20 + 22 = 42
Fim

Executar tarefas em paralelo com join!

O macro join! executa múltiplas futures concorrentemente:

use std::time::{Duration, Instant};
use tokio::time::sleep;

async fn buscar_usuario() -> String {
    sleep(Duration::from_millis(200)).await;
    "Maria Silva".to_string()
}

async fn buscar_pedidos() -> Vec<String> {
    sleep(Duration::from_millis(300)).await;
    vec!["Pedido #1".to_string(), "Pedido #2".to_string()]
}

async fn buscar_saldo() -> f64 {
    sleep(Duration::from_millis(150)).await;
    1500.50
}

#[tokio::main]
async fn main() {
    // Sequencial — cada await bloqueia o próximo
    let inicio = Instant::now();
    let _usuario = buscar_usuario().await;
    let _pedidos = buscar_pedidos().await;
    let _saldo = buscar_saldo().await;
    println!("Sequencial: {:?}", inicio.elapsed());

    // Paralelo com join! — todas executam ao mesmo tempo
    let inicio = Instant::now();
    let (usuario, pedidos, saldo) = tokio::join!(
        buscar_usuario(),
        buscar_pedidos(),
        buscar_saldo()
    );
    println!("Paralelo: {:?}", inicio.elapsed());

    println!("\nUsuário: {}", usuario);
    println!("Pedidos: {:?}", pedidos);
    println!("Saldo: R${:.2}", saldo);
}

Saída:

Sequencial: 650ms
Paralelo: 300ms

Usuário: Maria Silva
Pedidos: ["Pedido #1", "Pedido #2"]
Saldo: R$1500.50

Spawn de tarefas com tokio::spawn

Lance tarefas assíncronas independentes:

use tokio::time::{sleep, Duration};

async fn processar_item(id: u32) -> String {
    sleep(Duration::from_millis(100 * id as u64)).await;
    format!("Item {} processado", id)
}

#[tokio::main]
async fn main() {
    // Criar várias tarefas assíncronas
    let mut handles = Vec::new();

    for id in 1..=5 {
        // tokio::spawn lança uma tarefa no runtime
        let handle = tokio::spawn(async move {
            let resultado = processar_item(id).await;
            println!("  {}", resultado);
            resultado
        });
        handles.push(handle);
    }

    // Coletar resultados
    let mut resultados = Vec::new();
    for handle in handles {
        let resultado = handle.await.unwrap();
        resultados.push(resultado);
    }

    println!("\nTodos processados: {} tarefas", resultados.len());
}

Saída:

  Item 1 processado
  Item 2 processado
  Item 3 processado
  Item 4 processado
  Item 5 processado

Todos processados: 5 tarefas

select! — responder à primeira future

O macro select! executa múltiplas futures e age na primeira que completar:

use tokio::time::{sleep, Duration};

async fn servidor_principal() -> String {
    sleep(Duration::from_millis(300)).await;
    "Resposta do servidor principal".to_string()
}

async fn servidor_backup() -> String {
    sleep(Duration::from_millis(200)).await;
    "Resposta do servidor backup".to_string()
}

async fn timeout_seguranca() {
    sleep(Duration::from_millis(500)).await;
}

#[tokio::main]
async fn main() {
    // Pegar a resposta mais rápida
    let resultado = tokio::select! {
        resp = servidor_principal() => {
            println!("Principal respondeu primeiro");
            resp
        }
        resp = servidor_backup() => {
            println!("Backup respondeu primeiro");
            resp
        }
        _ = timeout_seguranca() => {
            println!("Timeout!");
            "Nenhum servidor respondeu".to_string()
        }
    };

    println!("Resultado: {}", resultado);
}

Saída:

Backup respondeu primeiro
Resultado: Resposta do servidor backup

Timeout em operações assíncronas

Adicione limites de tempo às suas operações:

use std::time::Duration;
use tokio::time::{sleep, timeout};

async fn operacao_lenta() -> String {
    sleep(Duration::from_secs(5)).await;
    "Concluído!".to_string()
}

async fn operacao_rapida() -> String {
    sleep(Duration::from_millis(100)).await;
    "Rápido!".to_string()
}

#[tokio::main]
async fn main() {
    // Operação com timeout
    match timeout(Duration::from_secs(1), operacao_lenta()).await {
        Ok(resultado) => println!("Sucesso: {}", resultado),
        Err(_) => println!("Timeout! Operação demorou mais de 1 segundo"),
    }

    // Operação que completa a tempo
    match timeout(Duration::from_secs(1), operacao_rapida()).await {
        Ok(resultado) => println!("Sucesso: {}", resultado),
        Err(_) => println!("Timeout!"),
    }
}

Saída:

Timeout! Operação demorou mais de 1 segundo
Sucesso: Rápido!

Canais assíncronos com mpsc

Comunique entre tarefas assíncronas:

use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};

#[derive(Debug)]
struct Mensagem {
    remetente: String,
    conteudo: String,
}

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<Mensagem>(32); // buffer de 32

    // Produtores
    for i in 1..=3 {
        let tx = tx.clone();
        tokio::spawn(async move {
            for j in 1..=2 {
                let msg = Mensagem {
                    remetente: format!("Produtor {}", i),
                    conteudo: format!("Mensagem {}", j),
                };
                tx.send(msg).await.unwrap();
                sleep(Duration::from_millis(50 * i as u64)).await;
            }
        });
    }

    // Dropar transmissor original
    drop(tx);

    // Consumidor
    let mut total = 0;
    while let Some(msg) = rx.recv().await {
        println!("[{}] {}", msg.remetente, msg.conteudo);
        total += 1;
    }

    println!("\nTotal recebido: {} mensagens", total);
}

Saída (ordem pode variar):

[Produtor 1] Mensagem 1
[Produtor 2] Mensagem 1
[Produtor 3] Mensagem 1
[Produtor 1] Mensagem 2
[Produtor 2] Mensagem 2
[Produtor 3] Mensagem 2

Total recebido: 6 mensagens

Quando usar threads vs async

CenárioMelhor opção
Muitas conexões de redeasync/await (tokio)
Cálculos pesados (CPU)std::thread ou rayon
Leitura de muitos arquivosasync/await (tokio)
Processamento de imagensrayon (thread pool)
Servidor webasync/await (axum/tokio)
Parser de dados grandesrayon para paralelismo de dados

Veja também