Bem-vindo a uma aula essencial para quem deseja dominar a programação assíncrona em Rust. Nesta aula, vamos explorar em profundidade o coração do modelo assíncrono: o trait Future. Entender como as futures funcionam é crucial para escrever código assíncrono eficiente, seguro e idiomático. Vamos começar desvendando a definição do trait e, em seguida, mergulharemos no mecanismo de Poll, nos estados que uma future pode assumir e, finalmente, no papel vital do Pin.

O modelo assíncrono do Rust é baseado em polling (consulta) em vez de callbacks ou async/await implícitos como em outras linguagens. Isso confere um controle fino sobre o fluxo de execução e permite que o Rust alcance desempenho previsível e sem custos ocultos. Nesta aula, você não apenas aprenderá a teoria, mas também verá exemplos práticos que ilustram cada conceito. Prepare-se para uma jornada que vai desde a definição do trait até a implementação manual de uma future simples.

O trait Future

Em Rust, uma future é uma computação assíncrona que pode ainda não ter produzido um resultado. O trait Future é a abstração central que define esse comportamento. Sua definição é surpreendentemente simples:

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Vamos analisar cada parte. O tipo associado Output representa o valor final produzido pela future. O método poll é a única operação obrigatória: ele recebe um Pin para uma referência mutável da própria future e um contexto (Context) que fornece acesso ao waker. O retorno é um Poll<Self::Output>, que pode ser Poll::Ready(valor) ou Poll::Pending.

Uma future não executa nada até que seja pollada (consultada). O executor é quem chama poll repetidamente até que a future esteja pronta. Isso é fundamental para entender: async blocks e async fn geram futures que implementam esse trait automaticamente, mas você também pode implementá-lo manualmente para casos especiais.

Um ponto importante é que o método poll nunca deve bloquear. Se a future não estiver pronta, ela deve retornar Poll::Pending e registrar o waker para ser acordada quando o recurso estiver disponível. Isso permite que o executor gerencie muitas futures em uma única thread sem bloquear.

Vejamos um exemplo de uma future manual simples que gera um número após um atraso (simulado por um contador de polls):

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

struct NumberFuture {
    polls: u32,
}

impl Future for NumberFuture {
    type Output = u32;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.polls += 1;
        if self.polls >= 3 {
            Poll::Ready(42)
        } else {
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    let future = NumberFuture { polls: 0 };
    let result = future.await;
    println!("Resultado: {}", result);
}

Esse exemplo é artificial, mas demonstra o ciclo de vida de uma future: ela é pollada várias vezes até retornar Ready. Na prática, você usaria async/await ou bibliotecas como tokio para criar futures, mas entender a implementação manual é valioso.

Poll

O enum Poll é a resposta do método poll. Sua definição é:

pub enum Poll<T> {
    Ready(T),
    Pending,
}

Quando uma future retorna Poll::Ready(valor), significa que ela completou e o valor está disponível. Quando retorna Poll::Pending, significa que ainda não está pronta e o executor deve agendar uma nova chamada a poll quando o waker for acionado.

O waker é a peça-chave para eficiência: em vez de ficar consultando em loop (busy-loop), o executor registra o waker e dorme até que algum evento (como I/O) acorde a future. O waker é passado dentro do Context e pode ser clonado e armazenado para uso posterior.

Vamos entender como o poll é usado na prática com um exemplo que simula uma operação de I/O assíncrona. Imagine que temos uma função que lê de um socket. O executor chamará poll quando houver dados disponíveis. Até lá, retorna Pending.

Um ponto crucial: poll deve ser chamado apenas com um Pin<&mut Self>. Isso nos leva ao tópico do Pin, mas primeiro vamos ver como o Poll se integra no ciclo de vida de uma future.

No código async, o compilador transforma o bloco em uma máquina de estados que implementa Future. Cada .await gera um ponto de suspensão onde a future retorna Pending se o recurso não estiver pronto. O waker é usado para retomar a execução.

Exemplo de uso de Poll em uma implementação manual:

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

struct Counter {
    count: u32,
}

impl Future for Counter {
    type Output = u32;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.count < 5 {
            self.count += 1;
            // Simula uma operação que pode não estar pronta
            cx.waker().wake_by_ref(); // acorda imediatamente para re-poll
            Poll::Pending
        } else {
            Poll::Ready(self.count)
        }
    }
}

#[tokio::main]
async fn main() {
    let counter = Counter { count: 0 };
    let result = counter.await;
    println!("Contagem final: {}", result);
}

Nesse exemplo, a future é acordada repetidamente até atingir 5. Isso é ineficiente, mas ilustra como o waker pode ser usado para agendar novas polls.

Estado das futures

Uma future pode estar em diferentes estados ao longo de sua vida. Embora não haja um enum explícito no trait, conceitualmente podemos pensar em três estados:

  • Pronta: a future já tem um valor disponível e retornará Poll::Ready na próxima poll.
  • Pendente: a future ainda não tem um valor, mas pode ser acordada quando o recurso estiver disponível.
  • Completa: após retornar Ready, a future não deve ser pollada novamente (embora seja possível, é um erro de lógica).

Para o executor, é importante saber quando uma future foi concluída para não pollá-la novamente. Em async/await, o compilador garante que, após a conclusão, a future não seja mais usada (a menos que seja reiniciada, o que não é comum).

Além disso, uma future pode ser cancelada se for dropped antes de concluir. Isso é comum em operações de timeout ou quando o escopo termina. O drop da future pode liberar recursos alocados.

Vamos ver um exemplo de uma future que transita entre pendente e pronta usando um canal:

use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};

struct ChannelFuture<T> {
    value: Option<T>,
    waker: Option<Waker>,
}

impl<T> ChannelFuture<T> {
    fn new() -> Self {
        Self { value: None, waker: None }
    }

    fn set_value(&mut self, value: T) {
        self.value = Some(value);
        if let Some(waker) = self.waker.take() {
            waker.wake();
        }
    }
}

impl<T> Future for ChannelFuture<T> {
    type Output = T;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
        if let Some(value) = self.value.take() {
            Poll::Ready(value)
        } else {
            self.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    let mut future = ChannelFuture::new();
    // Simula uma tarefa que preenche o valor depois
    tokio::spawn(async move {
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        // Precisamos de acesso à future, mas ela já foi movida para o main? 
        // Na prática, usaríamos um canal compartilhado. Este é um exemplo simplificado.
    });
    let result = future.await;
    println!("Valor recebido: {}", result);
}

Esse exemplo não é totalmente funcional, mas ilustra a transição de estado: inicialmente pendente, depois pronta quando o valor é definido.

Pin (visão geral)

O tipo Pin é fundamental para garantir que os dados de uma future não sejam movidos enquanto ela está sendo pollada. Isso é necessário porque as futures podem conter auto-referências (referências a si mesmas) após um await. Se a future fosse movida, essas referências se tornariam inválidas, causando comportamento indefinido.

O compilador usa Pin para garantir que, uma vez que uma future é criada, ela não pode ser movida até que seja dropada. Isso é feito através do tipo Pin<P>, onde P é um ponteiro (como &mut T ou Box<T>).

O trait Unpin é um marcador que indica que um tipo pode ser movido com segurança. A maioria dos tipos é Unpin, mas futures geradas por async/await podem não ser. Para criar uma future que não é Unpin, você pode usar Box::pin ou Pin::new.

Vamos ver um exemplo de como Pin é usado na prática:

use std::pin::Pin;

struct MyFuture {
    data: Vec<u8>,
}

impl MyFuture {
    fn poll(self: Pin<&mut Self>) {
        // Acessa os dados com segurança
        let this = self.get_mut();
        this.data.push(1);
    }
}

fn main() {
    let mut future = MyFuture { data: vec![] };
    let mut pinned = Pin::new(&mut future);
    pinned.poll();
    println!("Dados: {:?}", future.data);
}

Nesse exemplo, Pin::new cria um Pin a partir de uma referência mutável. Como MyFuture é Unpin (por padrão), podemos mover a future depois, mas o Pin garante que enquanto está pinada, não será movida.

Na prática, você raramente precisa lidar com Pin diretamente ao usar async/await, mas é essencial entender para implementar futures personalizadas ou usar APIs que exigem pinagem.

Boas práticas e observações finais

Ao trabalhar com futures, lembre-se de:

  • Nunca bloquear dentro de poll. Use operações não bloqueantes ou retorne Pending.
  • Sempre acordar o waker quando a future estiver pronta para evitar deadlocks.
  • Usar async/await sempre que possível, pois é mais legível e o compilador otimiza.
  • Prefira bibliotecas como tokio ou async-std para operações de I/O, em vez de implementar futures manualmente.
  • Tenha cuidado com o cancelamento: ao dropar uma future, recursos alocados devem ser liberados.

Exercícios

  1. Explique a diferença entre Poll::Ready e Poll::Pending e por que poll não deve bloquear.
  2. ✓ Resposta: Poll::Ready indica que a future completou e o valor está disponível; Poll::Pending indica que ainda não está pronta. poll não deve bloquear porque o executor precisa gerenciar muitas futures em uma única thread; bloquear impediria que outras futures progredissem. Em vez disso, a future deve retornar Pending e registrar o waker para ser acordada quando o recurso estiver disponível.
  3. Qual é o papel do waker no contexto de uma future? Por que é importante acordar o waker?
  4. ✓ Resposta: O waker é um mecanismo para o executor saber quando uma future pendente deve ser pollada novamente. Quando o recurso está disponível, a future chama waker.wake() para notificar o executor. Isso evita busy-loop e permite que o executor durma enquanto espera eventos, tornando o sistema eficiente. Sem o waker, o executor não saberia quando re-poll, causando deadlock ou desperdício de CPU.
  5. O que é o estado de uma future? Descreva os estados possíveis e como eles se relacionam com o ciclo de vida.
  6. ✓ Resposta: Uma future pode estar pendente (ainda não tem resultado), pronta (tem um valor disponível) ou completa (já retornou Ready). Pendente significa que a future ainda está aguardando um recurso; pronta significa que na próxima poll retornará Ready; completa significa que a future já produziu seu valor e não deve ser pollada novamente. O ciclo de vida começa pendente, transita para pronta quando o recurso está disponível, e depois para completa após a poll que retorna Ready.
  7. Por que o Pin é necessário em futures? O que aconteceria se uma future que contém auto-referências fosse movida?
  8. ✓ Resposta: Pin garante que os dados de uma future não sejam movidos enquanto ela é pollada. Isso é crucial porque futures podem conter referências a si mesmas (auto-referências) após um await. Se a future fosse movida, essas referências apontariam para endereços antigos, causando comportamento indefinido. Pin impede que isso aconteça, assegurando a segurança da memória.
  9. Escreva um exemplo de uma future manual que retorne Pending na primeira poll e Ready na segunda. Use Pin e Context.
  10. ✓ Resposta:
    use std::future::Future;
    use std::pin::Pin;
    use std::task::{Context, Poll};
    
    struct TwoPollFuture {
        first: bool,
    }
    
    impl Future for TwoPollFuture {
        type Output = &'static str;
    
        fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
            if self.first {
                Poll::Ready("done")
            } else {
                self.first = true;
                Poll::Pending
            }
        }
    }
    
    #[tokio::main]
    async fn main() {
        let future = TwoPollFuture { first: false };
        let result = future.await;
        println!("{}", result);
    }

Referências