Programação assíncrona é essencial para construir aplicações eficientes, especialmente em I/O. Em Rust, ela é implementada através de Futures e das palavras-chave async e .await. Diferente de threads, que são gerenciadas pelo sistema operacional, tarefas assíncronas são leves e cooperativas, permitindo escalar para milhares de conexões simultâneas.

Nesta aula, você entenderá os conceitos fundamentais: o que é um Future, como definir funções assíncronas, como aguardar resultados com .await e como executar essas tarefas usando um runtime como Tokio. Também compararemos com threads para escolher a abordagem certa.

Futures

Um Future em Rust representa um valor que pode não estar disponível ainda. É uma abstração para computação que pode ser concluída em algum momento futuro. A trait Future possui um método poll que verifica se o valor está pronto. Enquanto não está, a tarefa pode ceder o controle para outras tarefas.

Futures são lazy: eles não fazem nada até que sejam executados por um runtime. Você pode combiná-los usando combinadores como and_then, map, ou mais comumente, com async/.await.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MyFuture;

impl Future for MyFuture {
    type Output = i32;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Poll::Ready(42)
    }
}

// Exemplo de uso (requer um executor)
// let fut = MyFuture;
// let result = block_on(fut); // 42

async fn e .await

A palavra-chave async transforma uma função em uma função que retorna um Future. Dentro de uma função async, você pode usar .await para aguardar a conclusão de outro Future, sem bloquear a thread atual. O estado da função é salvo e restaurado pelo compilador.

async fn fetch_data(url: &str) -> String {
    // Simula uma operação assíncrona
    format!("Dados de {}", url)
}

async fn process() {
    let data = fetch_data("https://exemplo.com").await;
    println!("{}", data);
}

// Para executar, precisamos de um runtime:
// #[tokio::main]
// async fn main() {
//     process().await;
// }

O .await suspende a execução da função atual até que o Future esteja pronto. Enquanto isso, a thread pode executar outras tarefas, tornando o código eficiente.

Runtimes (tokio)

Um runtime é o executor que gerencia a execução de Futures. O mais popular em Rust é o Tokio. Ele fornece um agendador, temporizadores, I/O assíncrono e muito mais. Para usar Tokio, adicione ao Cargo.toml:

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

Exemplo de um programa Tokio:

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

#[tokio::main]
async fn main() {
    println!("Início");
    sleep(Duration::from_secs(1)).await;
    println!("Fim após 1 segundo");
}

Tokio usa um agendador work-stealing para distribuir tarefas entre threads, maximizando o uso da CPU. Você pode criar tarefas com tokio::spawn:

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("Tarefa assíncrona");
        42
    });
    let result = handle.await.unwrap();
    println!("Resultado: {}", result);
}

Diferença de threads

Threads são gerenciadas pelo sistema operacional e têm custo de criação e troca de contexto alto. Cada thread consome uma pilha separada (normalmente 1 MB ou mais). Já tarefas assíncronas são leves: cada uma ocupa apenas alguns bytes no heap para o estado da máquina de estados, e a troca é cooperativa (sem intervenção do SO).

Use threads para tarefas CPU-bound (computação pesada) que precisam de paralelismo real. Use async para tarefas I/O-bound (rede, disco) onde você espera muitos bloqueios. Async permite escalar para milhares de conexões com poucas threads.

CaracterísticaThreadsAsync/Tarefas
Custo de criaçãoAlto (pilha ~1MB)Baixo (alguns bytes)
Troca de contextoPreemptiva (SO)Cooperativa (no .await)
EscalabilidadeCentenas a milharesDezenas de milhares
Uso idealCPU-boundI/O-bound

Em Rust, você pode combinar ambos: usar async para I/O e threads para processamento pesado, com comunicação via canais.

Boas práticas

  • Não bloqueie o runtime com chamadas síncronas (ex: std::thread::sleep). Use tokio::time::sleep.
  • Evite block_on dentro de código assíncrono; prefira .await.
  • Para I/O, use bibliotecas assíncronas como tokio::fs, tokio::net.
  • Mantenha funções async curtas e focadas; combine-as com join! ou select!.

Referências

Exercícios

  1. Crie uma função assíncrona ler_arquivo que simula a leitura de um arquivo com tokio::time::sleep por 2 segundos e retorna uma String. Escreva um programa que a chame e imprima o resultado.

    ✓ Resposta:
    use tokio::time::{sleep, Duration};
    
    async fn ler_arquivo() -> String {
        sleep(Duration::from_secs(2)).await;
        String::from("Conteúdo do arquivo")
    }
    
    #[tokio::main]
    async fn main() {
        let conteudo = ler_arquivo().await;
        println!("{}", conteudo);
    }
  2. Explique a diferença entre thread::spawn e tokio::spawn. Dê um exemplo onde cada um é mais adequado.

    ✓ Resposta:

    thread::spawn cria uma thread do SO, cara e com pilha grande. Ideal para tarefas CPU-bound. tokio::spawn cria uma tarefa assíncrona leve, ideal para I/O-bound. Exemplo: servidor web (async) vs processamento de imagens (thread).

  3. Escreva um programa que usa tokio::join! para executar duas funções assíncronas concorrentemente: uma que espera 1 segundo e outra que espera 2 segundos. Meça o tempo total (deve ser ~2 segundos).

    ✓ Resposta:
    use tokio::time::{sleep, Duration};
    
    async fn tarefa1() {
        sleep(Duration::from_secs(1)).await;
        println!("Tarefa 1 concluída");
    }
    
    async fn tarefa2() {
        sleep(Duration::from_secs(2)).await;
        println!("Tarefa 2 concluída");
    }
    
    #[tokio::main]
    async fn main() {
        let inicio = std::time::Instant::now();
        tokio::join!(tarefa1(), tarefa2());
        println!("Tempo total: {:?}", inicio.elapsed());
    }
  4. O que acontece se você chamar .await em um Future que retorna Poll::Pending para sempre? Como evitar isso?

    ✓ Resposta:

    A tarefa ficará suspensa para sempre, nunca retornando. Para evitar, use timeouts com tokio::time::timeout ou garanta que o Future seja completado eventualmente.

  5. Implemente uma função assíncrona que recebe um vetor de URLs (strings) e baixa o conteúdo de cada uma concorrentemente usando tokio::spawn. Use reqwest para fazer as requisições (adicione a dependência).

    ✓ Resposta:
    use tokio::task;
    
    async fn baixar_url(url: String) -> String {
        let resp = reqwest::get(&url).await.unwrap();
        resp.text().await.unwrap()
    }
    
    #[tokio::main]
    async fn main() {
        let urls = vec![
            "https://example.com".to_string(),
            "https://httpbin.org/get".to_string(),
        ];
        let mut handles = vec![];
        for url in urls {
            handles.push(task::spawn(baixar_url(url)));
        }
        for handle in handles {
            let conteudo = handle.await.unwrap();
            println!("Conteúdo: {}", &conteudo[..50]);
        }
    }