Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pin

Вспомним, что асинхронная функция или блок создают тип, который реализует Future и содержит все локальные переменные. Некоторые из этих переменных могут содержать ссылки (указатели) на другие локальные переменные. Чтобы они оставались корректными, фьючерс никогда не должен перемещаться в другое место в памяти.

Чтобы предотвратить перемещение типа фьючерса в памяти, его можно опрашивать только через закреплённый (pinned) указатель. Pin — обёртка вокруг ссылки, которая запрещает все операции, перемещающие объект, на который она указывает, в другое место в памяти.

// Copyright 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0

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

// Элемент работы. В данном случае просто спим заданное время и отвечаем
// сообщением в канал `respond_on`.
#[derive(Debug)]
struct Work {
    input: u32,
    respond_on: oneshot::Sender<u32>,
}

// Рабочий, который слушает очередь заданий и выполняет их.
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; // Делаем вид, что работаем.
                work.respond_on
                    .send(work.input * 1000)
                    .expect("failed to send response");
                iterations += 1;
            }
            // TODO: сообщать количество итераций каждые 100 мс
        }
    }
}

// Запрашивающий, который запрашивает работу и ждёт её завершения.
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("failed to send on work queue");
    rx.await.expect("failed waiting for response")
}

#[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!("work result for iteration {i}: {resp}");
    }
}
This slide should take about 20 minutes.
  • Вы можете узнать в этом пример паттерна «актор». Акторы обычно вызывают select! в цикле.

  • Это сводка нескольких предыдущих уроков, так что не торопитесь.

    • Наивно добавьте _ = sleep(Duration::from_millis(100)) => { println!(..) } в select!. Это никогда не выполнится. Почему?

    • Вместо этого добавьте timeout_fut, содержащий этот фьючерс, вне loop:

      #![allow(unused)]
      fn main() {
      // Copyright 2024 Google LLC
      // SPDX-License-Identifier: Apache-2.0
      
      let timeout_fut = sleep(Duration::from_millis(100));
      loop {
          select! {
              ..,
              _ = timeout_fut => { println!(..); },
          }
      }
      }
    • Это всё ещё не работает. Следуйте сообщениям компилятора: добавьте &mut к timeout_fut в select!, чтобы обойти перемещение, а затем используйте Box::pin:

      #![allow(unused)]
      fn main() {
      // Copyright 2024 Google LLC
      // SPDX-License-Identifier: Apache-2.0
      
      let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
      loop {
          select! {
              ..,
              _ = &mut timeout_fut => { println!(..); },
          }
      }
      }
    • Теперь это компилируется, но после истечения таймаута он Poll::Ready на каждой итерации (fused-фьючерс здесь помог бы). Обновите, чтобы сбрасывать timeout_fut каждый раз, когда он истекает:

      #![allow(unused)]
      fn main() {
      // Copyright 2024 Google LLC
      // SPDX-License-Identifier: Apache-2.0
      
      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)));
              },
          }
      }
      }
  • Box выделяет память в куче. В некоторых случаях также подходит std::pin::pin! (стабилизирован совсем недавно; в старом коде часто используют tokio::pin!), но его сложно использовать для фьючерса, которому присваивают новое значение.

  • Другая альтернатива — не использовать pin вообще, а запустить ещё одну задачу, которая будет отправлять сообщение в канал oneshot каждые 100 мс.

  • Данные, которые содержат указатели на самих себя, называются самоссылающимися. Обычно анализатор заимствований Rust не позволил бы переместить самоссылающиеся данные, так как ссылки не могут пережить данные, на которые указывают. Однако преобразование кода для асинхронных блоков и функций анализатором заимствований не проверяется.

  • Pin — обёртка вокруг ссылки. Объект нельзя переместить с его места с помощью закреплённого указателя. Однако его по-прежнему можно переместить через незакреплённый указатель.

  • Метод poll трейта Future использует Pin<&mut Self> вместо &mut Self, чтобы ссылаться на экземпляр. Поэтому его можно вызвать только через закреплённый указатель.