نگاه دقیق به Future Trait

trait Future در Rust هسته مرکزیه برنامه نویسی Async محسوب میشه. Future یک محاسبه Async هست که در نهایت یک مقداری رو تولید میکنه.(که البته ممکنه اون مقدار خالی باشه مثل ()). یک مثال آسون شده از trait Future میتونه این شکلی باشه:


#![allow(unused)]
fn main() {
trait SimpleFuture {
    type Output;
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

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

Future ها میتونن با استفاده از صدا زدن تابع poll اجرا بشن، که این باعث میشه Future رو همینجور به سمتی سوق بده که بالاخره اجراش تکمیل بشه. اگه Future تموم بشه مقدار Poll::Ready(result) رو برمیگردونه. اگه Future هنوز آماده نیست و تکمیل نشده باشه مقدار Poll::Pending رو برمیگردونه و هر وقت Future آماده باشه تا پردازش بیشتری رو جلو ببره تابع wake() رو صدا میزنه. وقتی wake() صدا زده بشه، اون اجراکننده ای که داره Future رو انجام میده دوباره poll رو صدا میزنه تا Future بتونه پزدازششو جلو ببره.

بدون wake() اون اجراکننده هیچ اطلاعی نداره که یه Future خاص میتونه ادامه پردازششو انجام بده، و باید به صورت مداوم همه future ها رو poll بگیره. با تابع wake() اجراکننده دقیقا میدونه کدوم future آماده هست تا poll گرفته بشه.

برای مثال فرض کنیید قراره از یک socket دیتایی رو بخونیم که هنوز آماده نیست. اگه دیتا آماده باشه میتونیم بخونیمش و مقدار Poll::Ready(data) ولی اگه دیتایی آماده نباشه future ما بلاک و مسدود میشه و نمیتونم پردازش رو جلو ببریم. وقتی دیتایی آماده نباشه ما باید یک wake رو ثبت کنیم یا به اصطلاح register کنیم تا وقتی دیتا socket آماده بود اونو صدا بزنیم، که این کار باعث میشه به اجراکننده کدمون این پیغامو برسونه که future آماده ادامه پردازششه. یه مثال ساده از SocketRead میتونه چیزی شبیه به مثال زیر باشه:

pub struct SocketRead<'a> {
    socket: &'a Socket,
}

impl SimpleFuture for SocketRead<'_> {
    type Output = Vec<u8>;

    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if self.socket.has_data_to_read() {
            // سوکت دیتا رو آماده داره -- بخونش داخل یه بافر و برگردونش
            Poll::Ready(self.socket.read_buf())
        } else {
            // سوکت هنوز دیتایی نداره
            //
            // تابع "بیدار شدن" رو زمانی که دیتا آماده بود صدا بزن.
            // وقتی دیتا آماده باشه تابع "بیدار شدن" صدا زده میشه و در نتیجه
            // اون "فیوچر" میدونه که الان باید "پول" رو صدا بزنه دوباره و دیتا رو بگیره
            self.socket.set_readable_callback(wake);
            Poll::Pending
        }
    }
}

این مدل از Future ها امکان ترکیب چندین عملیات async رو بدون نیاز به allocate کردن state های اضافی فراهم می کنند. اجرا کردن چندین Future به صورت یکجا یا به صورت زنجیره ای از Future ها میتونه بدون نیاز به allocation های اضافی state های مربوطه به صورت زیر پیاده سازی بشه:

/// یک "فیوچر ساده" که دو تا تابع "فیوچر" دیگه رو اجرا میکنه تا همزمان تموم بشن
/// 
/// همزمانی در اینجا با استفاده از صدا کردن "پول" روی تک تک "فیوچر" ها
/// انجام میشه، که این اجازه رو میده که اگر "فیوچر"ی خواست میتونه دیگه اجرا نشه و بقیه
/// اجرا بشن و در واقع هر "فیوچر"ی با سرعت خودش اجرا بشه
pub struct Join<FutureA, FutureB> {
    // هر کدوم از فیلد ها زیر ممکنه توشون "فیوچر" ی باشه که باید اجرا شه
    // اگر "فیوچرز" تکمیل شده باشه مقدار فیلد به "نان" تغییر پیدا میکنه
    // که این باعث میشه ما "فیوچر" هایی که تکمیل شدن رو دوباره اجرا نکنیم و "پول" نگیریم
    a: Option<FutureA>,
    b: Option<FutureB>,
}

impl<FutureA, FutureB> SimpleFuture for Join<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        // سعی کن "فیوچر" اول رو انجلم بدی
        if let Some(a) = &mut self.a {
            if let Poll::Ready(()) = a.poll(wake) {
                self.a.take();
            }
        }

        // سعی کن "فیوچر" بعدی رو انجام بدی
        if let Some(b) = &mut self.b {
            if let Poll::Ready(()) = b.poll(wake) {
                self.b.take();
            }
        }

        if self.a.is_none() && self.b.is_none() {
            // هر دوی "فیوچر" ها با موفقیت تموم شدن -- حالا میتونیم با موفقیت نتیجه رو برگردونیم
            Poll::Ready(())
        } else {
            // یک یا هر دوی "فیوچر" ها آماده نیست و هنوز کار دارن تا تموم بشن
            // اونا تابع "بیدار شدن" رو زمانی که بتونن پردازشی رو جلو ببرن صدا میزنن
            Poll::Pending
        }
    }
}

این نشون دهنده اینه که چندین Future میتونن به صورت همزمان اجرا بشن بدون نیاز به allocation های جداگانه، که همین باعث میشه برنامه های async با بازده بیشتر رو بتونیم بسازیم. مشابه همین داستان، چندین Future میتونن به صورت خطی پشت سر هم اجرا بشن، مثل مثال زیر:

/// یک "فیچوچر" دیگه که دو تا "فیوچر" رو انجام میده، یکی بعد از اونی یک و پشت سر هم
//
// نکته: برای اهداف این مثال هر دوی "فیوچر" ها در لحظه ساختن در دسترس هستن
// در واقعیت خروجی "فویچر" دوم میتونه به عنوان ورودی به "فیوچر" بعدئی منتقل شه
pub struct AndThenFut<FutureA, FutureB> {
    first: Option<FutureA>,
    second: FutureB,
}

impl<FutureA, FutureB> SimpleFuture for AndThenFut<FutureA, FutureB>
where
    FutureA: SimpleFuture<Output = ()>,
    FutureB: SimpleFuture<Output = ()>,
{
    type Output = ();
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
        if let Some(first) = &mut self.first {
            match first.poll(wake) {
                // ما اولین "فیوچر" رو انجام دادیم -- پاکش کن
                // و دومی رو شروع کن
                Poll::Ready(()) => self.first.take(),
                // ما هنوز نمیتونیم اولین "فیوچر" روو انجام بدیم
                Poll::Pending => return Poll::Pending,
            };
        }
        // حالا که اولین "فیوچر" تموم شده، سعی کن دومی رو انجام بدی
        self.second.poll(wake)
    }
}

این مثال ها نشون میده چطوری trait Future میتونه بیان های مختلفی از جریان کنترلی روی یه برنامه async رو بدون نیاز به allocate کردن چندین object و callback های تو در تو ارائه بده. با این تفاسیر بیاید راجب trait اصلی Future و تفاوت هاش صحبت کنیم:

trait Future {
    type Output;
    fn poll(
        // به تغییر تایپ این مقدار پایین دقیت کنید
        self: Pin<&mut Self>,
        // تایپ مقدار پایین هم عوض شده
        cx: &mut Context<'_>,
    ) -> Poll<Self::Output>;
}

اولین چیزی که احتمالا متوجهش شدید اینه که self دیگه از نوع &mut Self نیست و تبدیل شده به Pin<&mut Self>. ما راجب Pin کردن بیشتر در اینده صحبت میکنم، ولی فعلا در همین حد بدونید که Pin کردن این اجازه رو به ما میده تا Future های غیر قابل حرکت (ثابت) بسازیم. Object های ثابت یا غیر قابل حرکت میتونن Pointer مربوط به مقادیرشون رو بین همون مقادیر ذخیره کنن مثلا struct MyFut { a: i32, ptr_to_a: *const i32 }. Pin کردن یه چیز واجب برای استفاده از async/await هست.

در ادامه میبینم که wake: fn() تبدیل شده به &mut Context<'_>. در SimpleFuture ما یک تابع pointer (fn()) رو صدا میزدیم تا به اون اجراکننده Future بگیم اون Future ای که نیاز داریم رو poll بگیره یا وضعیتشو چک کنه. با این وجود، چون fn() فقط یه تابع pointer هست، نمیتونه هیچ اطلاعاتی راجب اینکه کدوم Future تابع wake رو صدا زده ذخیره کنه.

توی یک نرم افزار واقعی که قراره ساخته بشه، یه برنامه پیچیده مثل یک وب سرور شاید هزاران کانکشن داره که اون کانکشن ها باید توابع wake up شون به صورت جداگانه همگی صدا زده بشه. تایپ Context این مشکل رو با استفاده از تایپ Waker و دسترسی به اون حل کرده، که در حقیقت میتونه باهاش یه task خاص و مشخص رو صدا بزنه و wake up اش کنه.