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}");
}
}
-
Вы можете узнать в этом пример паттерна «актор». Акторы обычно вызывают
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, чтобы ссылаться на экземпляр. Поэтому его можно вызвать только через закреплённый указатель.