Pin

Os blocos e funções async retornam tipos que implementam o trait Future. O tipo retornado é o resultado de uma transformação do compilador que transforma variáveis locais em dados armazenados dentro da future.

Algumas dessas variáveis podem conter ponteiros para outras variáveis locais. Por causa disso, a future nunca deve ser movida para uma localização de memória diferente, pois isso invalidaria esses ponteiros.

Para evitar mover o tipo de future na memória, ele só pode ser poll por meio de um ponteiro pinned. Pin é um invólucro em torno de uma referência que proíbe todas as operações que moveriam a instância para a qual aponta para uma localização de memória diferente.

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

// Um item de trabalho. Neste caso, apenas dormir pelo tempo dado e responder com uma mensagem no canal `respond_on`.
#[derive(Debug)]
struct Work {
    input: u32,
    respond_on: oneshot::Sender<u32>,
}

// Um trabalhador que escuta o trabalho em uma fila e o executa.
async fn worker(mut work_queue: mpsc::Receiver<Work>) {
    let mut iterations = 0;
    loop {
        tokio::select! {
            Some(work) = work_queue.recv() => {
                sleep(Duration::from_millis(10)).await; // Fingir trabalhar.
                work.respond_on
                    .send(work.input * 1000)
                    .expect("falha ao enviar resposta");
                iterations += 1;
            }
            // TODO: relatar o número de iterações a cada 100ms
        }
    }
}

// Um solicitante que solicita trabalho e aguarda sua conclusão.
async fn do_work(work_queue: &mpsc::Sender<Work>, input: u32) -> u32 {
    let (tx, rx) = oneshot::channel();
    work_queue
        .send(Work { input, respond_on: tx })
        .await
        .expect("falha ao enviar na fila de trabalho");
    rx.await.expect("falha ao esperar pela resposta")
}

#[tokio::main]
async fn main() {
    let (tx, rx) = mpsc::channel(10);
    spawn(worker(rx));
    for i in 0..100 {
        let resp = do_work(&tx, i).await;
        println!("resultado do trabalho para a iteração {i}: {resp}");
    }
}
This slide should take about 20 minutes.
  • Você pode reconhecer isso como um exemplo do padrão actor. Os actors tipicamente chamam select! em um loop.

  • Isso serve como uma síntese de algumas das lições anteriores, então leve seu tempo com isso.

    • Adicione ingenuamente um _ = sleep(Duration::from_millis(100)) => { println!(..) } ao select!. Isso nunca será executado. Por quê?

    • Em vez disso, adicione um timeout_fut contendo essa future fora do loop:

      #![allow(unused)]
      fn main() {
      let timeout_fut = sleep(Duration::from_millis(100));
      loop {
          select! {
              ..,
              _ = timeout_fut => { println!(..); },
          }
      }
      }
    • Isso ainda não funciona. Siga os erros do compilador, adicionando &mut ao timeout_fut no select! para contornar a movimentação, e então usando Box::pin.

      #![allow(unused)]
      fn main() {
      let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
      loop {
          select! {
              ..,
              _ = &mut timeout_fut => { println!(..); },
          }
      }
      }
    • Isso compila, mas uma vez que o timeout expira, ele é Poll::Ready em cada iteração (um fused future ajudaria com isso). Atualize para redefinir timeout_fut toda vez que expirar:

      #![allow(unused)]
      fn main() {
      let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
      loop {
          select! {
              _ = &mut timeout_fut => {
                  println!(..);
                  timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
              },
          }
      }
      }
  • O Box aloca na pilha. Em alguns casos, std::pin::pin! (apenas recentemente estabilizado, com código mais antigo frequentemente usando tokio::pin!) também é uma opção, mas é difícil de usar para uma future que é reatribuída.

  • Outra alternativa é não usar pin de forma alguma, mas iniciar outra tarefa que enviará para um canal oneshot a cada 100ms.

  • Dados que contêm ponteiros para si mesmos são chamados auto-referenciais. Normalmente, o verificador de empréstimos do Rust impediria que dados auto-referenciais fossem movidos, pois as referências não podem sobreviver aos dados aos quais apontam. No entanto, a transformação de código para blocos e funções async não é verificada pelo verificador de empréstimos.

  • Pin é um invólucro em torno de uma referência. Um objeto não pode ser movido de seu local usando um ponteiro pinned. No entanto, ele ainda pode ser movido por meio de um ponteiro não pinned.

  • O método poll do trait Future usa Pin<&mut Self> em vez de &mut Self para se referir à instância. É por isso que ele só pode ser chamado em um ponteiro pinned.