Pin

بلوک‌ها و توابع Async انواعی را برمی‌گردانند که ویژگی Future را پیاده‌سازی می‌کنند. نوع برگشتی نتیجه تبدیل کامپایلر است که متغیرهای محلی را به داده های ذخیره شده در future تبدیل می کند.

برخی از این متغیرها می‌توانند اشاره‌گرهایی را برای سایر متغیرهای محلی نگه دارند. به همین دلیل، future هرگز نباید به مکان حافظه دیگری منتقل شود، زیرا این pointerها را باطل می‌کند.

برای جلوگیری از جابجایی تایپ future در حافظه، فقط از طریق یک pointer پین شده می توان آن را بررسی کرد. Pin یک wrapper در اطراف یک reference است که تمام عملیاتی را که می‌تواند نمونه‌ای را که به آن اشاره می‌کند به یک مکان حافظه متفاوت منتقل کند را ممنوع می‌کند.

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

// A work item. In this case, just sleep for the given time and respond
// with a message on the `respond_on` channel.
#[derive(Debug)]
struct Work {
    input: u32,
    respond_on: oneshot::Sender<u32>,
}

// A worker which listens for work on a queue and performs it.
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; // Pretend to work.
                work.respond_on
                    .send(work.input * 1000)
                    .expect("failed to send response");
                iterations += 1;
            }
            // TODO: report number of iterations every 100ms
        }
    }
}

// A requester which requests work and waits for it to complete.
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!("نتیجه کار برای تکرار  {i}: {resp}");
    }
}
This slide should take about 20 minutes.
  • شما ممکن است این را به عنوان نمونه ای از الگوی بازیگر (actor pattern) تشخیص دهید. بازیگران معمولاً select! را در یک حلقه صدا می‌زنند.

  • این به عنوان یک جمع‌بندی از چند درس قبلی عمل می کند، بنابراین وقت خود را صرف آن کنید.

    • به‌سادگی یک _ = sleep(Duration::from_millis(100)) => { println!(..) } را به select! اضافه کنید. این مورد هرگز اجرا نمی شود. چرا؟

    • درعوض، یک timeout_fut حاوی آن future خارج از loop اضافه کنید:

      #![allow(unused)]
      fn main() {
      let timeout_fut = sleep(Duration::from_millis(100));
      loop {
          select! {
              ..,
              _ = timeout_fut => { println!(..); },
          }
      }
      }
    • This still doesn't work. Follow the compiler errors, adding &mut to the timeout_fut in the select! to work around the move, then using Box::pin:

      #![allow(unused)]
      fn main() {
      let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100)));
      loop {
          select! {
              ..,
              _ = &mut timeout_fut => { println!(..); },
          }
      }
      }
    • این مورد کامپایل می‌شود، اما پس از انقضای timeout و در هر تکرار برابر با Poll::Ready است (future ترکیبی به این مسئله کمک می‌کند). به‌روزرسانی برای بازنشانی timeout_fut هر بار که منقضی می‌شود:

      #![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)));
              },
          }
      }
      }
  • Box allocates on the heap. In some cases, std::pin::pin! (only recently stabilized, with older code often using tokio::pin!) is also an option, but that is difficult to use for a future that is reassigned.

  • جایگزین دیگر این است که به هیچ وجه از pin استفاده نکنید، بلکه task دیگری ایجاد کنید که هر 100 میلی‌ثانیه به یک کانال oneshot ارسال می‌شود.

  • داده‌هایی که حاوی اشاره‌گرهایی به خود هستند، خود ارجاعی ( self-referential) نامیده می شوند. به طور معمول، Rust borrow checker از جابجایی داده‌های خودارجاعی جلوگیری می‌کند، زیرا منابع نمی‌توانند بیشتر از داده‌هایی که به آنها اشاره می‌کنند زنده بمانند. با‌این‌حال، تبدیل کد برای بلوک‌ها و توابع async توسط borrow checker تأیید نمی‌شود.

  • Pin یک wrapper در اطراف یک reference است. یک object را نمی‌توان با استفاده از یک pointer پین شده از جای خود حرکت داد. با این حال، هنوز هم می‌توان آن را از طریق یک pointer بدون پین جابجا کرد.

  • متد poll از ویژگی Future از Pin<&mut Self> به جای &mut Self برای اشاره به نمونه (instance) استفاده می‌‌کند. به همین دلیل است که فقط می‌توان آن را روی یک اشاره‌‌گر پین شده فراخوانی کرد.