بررسی دقیق‌تر ویژگی‌ها برای Async

در طول این فصل، از ویژگی‌های Future، Pin، Unpin، Stream، و StreamExt به روش‌های مختلفی استفاده کرده‌ایم. تاکنون، از ورود بیش از حد به جزئیات نحوه کارکرد یا چگونگی تطبیق آن‌ها با یکدیگر اجتناب کرده‌ایم، که برای بیشتر کارهای روزمره شما با Rust کافی است. با این حال، گاهی اوقات با موقعیت‌هایی مواجه می‌شوید که نیاز دارید کمی بیشتر از این جزئیات را بفهمید. در این بخش، به اندازه‌ای به این موضوع می‌پردازیم که در این سناریوها کمک کند، در حالی که بررسی عمیق‌تر را به مستندات دیگر می‌سپاریم.

ویژگی Future

بیایید با بررسی دقیق‌تر نحوه عملکرد ویژگی Future شروع کنیم. در اینجا نحوه تعریف آن در Rust آمده است:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

این تعریف Trait شامل چندین نوع جدید و همچنین نحوی است که قبلاً ندیده‌ایم، بنابراین بیایید قطعه به قطعه آن را بررسی کنیم.

ابتدا، نوع وابسته Output در ویژگی Future مشخص می‌کند که نتیجه future چه خواهد بود. این شبیه به نوع وابسته Item در ویژگی Iterator است. دوم، ویژگی Future همچنین متد poll را دارد که یک مرجع خاص Pin برای پارامتر self و یک مرجع متغیر به نوع Context می‌گیرد و یک Poll<Self::Output> بازمی‌گرداند. در ادامه درباره Pin و Context بیشتر صحبت خواهیم کرد. فعلاً بیایید روی چیزی که متد بازمی‌گرداند، یعنی نوع Poll، تمرکز کنیم:

#![allow(unused)]
fn main() {
enum Poll<T> {
    Ready(T),
    Pending,
}
}

نوع Poll شبیه به یک Option است. این نوع دو حالت دارد: یکی Ready(T) که شامل یک مقدار است و دیگری Pending که شامل مقدار نیست. با این حال، Poll معنای کاملاً متفاوتی از Option دارد! حالت Pending نشان می‌دهد که future هنوز کارهایی برای انجام دادن دارد، بنابراین فراخواننده باید بعداً دوباره بررسی کند. حالت Ready نشان می‌دهد که future کار خود را به پایان رسانده و مقدار T در دسترس است.

نکته: برای بیشتر futures، فراخواننده نباید پس از اینکه future مقدار Ready بازگرداند، دوباره poll را فراخوانی کند. بسیاری از futures اگر پس از آماده شدن دوباره poll شوند، دچار وحشت (panic) می‌شوند. futuresی که ایمن برای poll دوباره هستند، به‌طور صریح این موضوع را در مستندات خود ذکر خواهند کرد. این شبیه به نحوه رفتار Iterator::next است.

وقتی کدی را می‌بینید که از await استفاده می‌کند، Rust آن را در پشت صحنه به کدی که poll را فراخوانی می‌کند کامپایل می‌کند. اگر به لیست ۱۷-۴ که در آن عنوان صفحه برای یک URL واحد پس از حل‌شدن چاپ شد، نگاهی بیندازید، Rust آن را به چیزی که (اگرچه دقیقاً نه، اما تقریباً) شبیه به این است کامپایل می‌کند:

match page_title(url).poll() {
    Ready(page_title) => match page_title {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
    Pending => {
        // But what goes here?
    }
}

وقتی که future هنوز در حالت Pending است، چه کاری باید انجام دهیم؟ نیاز داریم به نوعی دوباره امتحان کنیم، و این کار را بارها تکرار کنیم، تا زمانی که future در نهایت آماده شود. به عبارت دیگر، نیاز به یک حلقه داریم:

let mut page_title_fut = page_title(url);
loop {
    match page_title_fut.poll() {
        Ready(value) => match page_title {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
        Pending => {
            // continue
        }
    }
}

اگر Rust دقیقاً این کد را کامپایل می‌کرد، هر await مسدودکننده (blocking) می‌شد—دقیقاً برعکس چیزی که می‌خواستیم! در عوض، Rust اطمینان حاصل می‌کند که حلقه بتواند کنترل را به چیزی واگذار کند که بتواند کار روی این future را متوقف کرده، روی futures دیگر کار کند، و سپس دوباره این یکی را بررسی کند. همان‌طور که دیدیم، این وظیفه یک runtime async است، و این برنامه‌ریزی و هماهنگی یکی از وظایف اصلی آن است.

در ابتدای فصل، درباره انتظار برای rx.recv صحبت کردیم. فراخوانی recv یک future بازمی‌گرداند و منتظر شدن برای future آن را poll می‌کند. اشاره کردیم که یک runtime future را تا زمانی که آماده شود—چه با Some(message) یا با None در صورت بسته شدن کانال—متوقف می‌کند. با درک عمیق‌تر از ویژگی Future و به‌طور خاص Future::poll، می‌توانیم ببینیم این چگونه کار می‌کند. وقتی future مقدار Poll::Pending بازمی‌گرداند، runtime می‌داند که آماده نیست. برعکس، وقتی poll مقدار Poll::Ready(Some(message)) یا Poll::Ready(None) بازمی‌گرداند، runtime می‌داند که future آماده است و آن را پیش می‌برد.

جزئیات دقیق نحوه انجام این کار توسط یک runtime فراتر از محدوده این کتاب است، اما نکته کلیدی این است که مکانیک پایه‌ای futures را ببینیم: یک runtime هر future که مسئول آن است را poll می‌کند و وقتی هنوز آماده نیست، future را دوباره به حالت خواب می‌برد.

ویژگی‌های Pin و Unpin

وقتی مفهوم pinning را در لیست ۱۷-۱۶ معرفی کردیم، با یک پیام خطای بسیار پیچیده مواجه شدیم. در اینجا بخش مرتبط با آن دوباره آمده است:

error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
  --> src/main.rs:48:33
   |
48 |         trpl::join_all(futures).await;
   |                                 ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async block@src/main.rs:10:23: 10:33}>: Future`
   |
   = note: consider using the `pin!` macro
           consider using `Box::pin` if you need to access the pinned value outside of the current scope
   = note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
  --> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/future/join_all.rs:29:8
   |
27 | pub struct JoinAll<F>
   |            ------- required by a bound in this struct
28 | where
29 |     F: Future,
   |        ^^^^^^ required by this bound in `JoinAll`

این پیام خطا نه تنها به ما می‌گوید که باید مقادیر را pin کنیم، بلکه دلیل نیاز به pinning را نیز توضیح می‌دهد. تابع trpl::join_all یک ساختار به نام JoinAll بازمی‌گرداند. این ساختار به نوعی عمومی به نام F وابسته است که محدود به پیاده‌سازی ویژگی Future است. منتظر شدن مستقیم یک future با await، future را به‌طور ضمنی pin می‌کند. به همین دلیل نیازی نیست که از pin! در همه جاهایی که می‌خواهیم برای futures منتظر بمانیم، استفاده کنیم.

با این حال، ما اینجا مستقیماً منتظر یک future نیستیم. در عوض، یک future جدید به نام JoinAll می‌سازیم با ارسال مجموعه‌ای از futures به تابع join_all. امضای join_all نیاز دارد که نوع آیتم‌های مجموعه، ویژگی Future را پیاده‌سازی کنند، و Box<T> فقط در صورتی ویژگی Future را پیاده‌سازی می‌کند که T که بسته‌بندی می‌کند، یک future باشد که ویژگی Unpin را پیاده‌سازی کرده است.

این اطلاعات زیادی برای هضم کردن است! برای درک واقعی آن، بیایید کمی بیشتر به نحوه کار واقعی ویژگی Future، به‌ویژه در ارتباط با pinning، بپردازیم.

دوباره به تعریف ویژگی Future نگاه کنید:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    // متد مورد نیاز
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}

پارامتر cx و نوع آن، Context، کلید اصلی برای این است که یک runtime چگونه می‌داند چه زمانی یک future خاص را بررسی کند، در حالی که همچنان تنبلی (lazy) باقی می‌ماند. باز هم، جزئیات نحوه کار این فرآیند فراتر از محدوده این فصل است، و معمولاً تنها زمانی که بخواهید یک پیاده‌سازی سفارشی برای Future بنویسید، نیاز به فکر کردن به این موضوع دارید. در عوض، ما بر روی نوع self تمرکز می‌کنیم، زیرا این اولین باری است که یک متد با یک نوع مشخص برای self روبرو می‌شویم. یک نوع مشخص برای self مانند نوع‌های مشخص برای سایر پارامترهای تابع عمل می‌کند، اما با دو تفاوت کلیدی:

  • به Rust می‌گوید که نوع self برای فراخوانی متد باید چه باشد.

  • نمی‌تواند هر نوعی باشد. این نوع به نوعی که متد روی آن پیاده‌سازی شده است، یا یک مرجع یا اشاره‌گر هوشمند به آن نوع، یا یک Pin که یک مرجع به آن نوع را بسته‌بندی می‌کند، محدود است.

در فصل ۱۸ بیشتر درباره این سینتکس صحبت خواهیم کرد. فعلاً کافی است بدانیم که اگر بخواهیم یک future را poll کنیم تا بررسی کنیم که آیا Pending یا Ready(Output) است، به یک مرجع متغیر بسته‌بندی‌شده در Pin برای آن نوع نیاز داریم.

Pin یک بسته‌بندی برای انواع اشاره‌گر مانند &، &mut، Box، و Rc است. (به‌طور فنی، Pin با نوع‌هایی کار می‌کند که ویژگی‌های Deref یا DerefMut را پیاده‌سازی می‌کنند، اما این به طور مؤثر معادل کار با اشاره‌گرها است.) Pin خودش یک اشاره‌گر نیست و هیچ رفتاری مانند Rc و Arc که شمارش مرجع انجام می‌دهند ندارد؛ این صرفاً یک ابزار است که کامپایلر می‌تواند برای اعمال محدودیت‌ها در استفاده از اشاره‌گرها استفاده کند.

به یاد آوردن این که await بر اساس فراخوانی‌های poll پیاده‌سازی شده است، شروع به توضیح پیام خطایی که قبلاً دیدیم می‌کند، اما آن پیام در مورد Unpin بود، نه Pin. پس دقیقاً چگونه Pin با Unpin مرتبط است، و چرا Future نیاز دارد که self در یک نوع Pin باشد تا بتواند poll را فراخوانی کند؟

به یاد بیاورید که در اوایل این فصل، یک سری از نقاط انتظار (await points) در یک future به یک ماشین حالت کامپایل می‌شوند، و کامپایلر اطمینان حاصل می‌کند که این ماشین حالت تمام قوانین معمول ایمنی Rust، از جمله قرض‌گیری و مالکیت، را دنبال می‌کند. برای اینکه این کار انجام شود، Rust بررسی می‌کند که چه داده‌ای بین یک نقطه انتظار و یا نقطه انتظار بعدی یا پایان بلوک async مورد نیاز است. سپس یک حالت متناظر در ماشین حالت کامپایل‌شده ایجاد می‌کند. هر حالت دسترسی لازم به داده‌هایی که در آن بخش از کد منبع استفاده می‌شوند را دریافت می‌کند، چه با گرفتن مالکیت آن داده‌ها یا با دریافت یک مرجع متغیر یا غیرمتغیر به آن.

تا اینجا خوب است: اگر در مورد مالکیت یا مراجع در یک بلوک async خطایی داشته باشیم، borrow checker به ما اطلاع می‌دهد. اما وقتی بخواهیم futureای که به آن بلوک مربوط می‌شود را جابه‌جا کنیم—مثلاً آن را به یک ساختار داده push کنیم تا به‌عنوان یک iterator با join_all استفاده شود یا آن را از یک تابع بازگردانیم—مسائل پیچیده‌تر می‌شوند.

وقتی یک future را جابه‌جا می‌کنیم—چه با push کردن آن به یک ساختار داده برای استفاده به‌عنوان iterator با join_all یا با بازگرداندن آن از یک تابع—این در واقع به معنای جابه‌جا کردن ماشین حالتی است که Rust برای ما ایجاد می‌کند. و برخلاف بیشتر انواع دیگر در Rust، futureهایی که Rust برای بلوک‌های async ایجاد می‌کند، می‌توانند در فیلدهای هر حالت معین، دارای مراجع به خودشان باشند، همان‌طور که در تصویر ساده‌شده‌ای که در شکل ۱۷-۴ نشان داده شده است.

A single-column, three-row table representing a future, fut1, which has data values 0 and 1 in the first two rows and an arrow pointing from the third row back to the second row, representing an internal reference within the future.
شکل 17-4: یک نوع داده خودارجاعی.

به‌طور پیش‌فرض، هر شیئی که مرجعی به خودش دارد، جابه‌جا کردن آن ناایمن است، زیرا مراجع همیشه به آدرس حافظه واقعی چیزی که به آن اشاره می‌کنند اشاره دارند (نگاه کنید به شکل ۱۷-۵). اگر خود ساختار داده را جابه‌جا کنید، آن مراجع داخلی همچنان به مکان قدیمی اشاره می‌کنند. با این حال، آن مکان حافظه اکنون نامعتبر است. از یک طرف، مقدار آن هنگام ایجاد تغییرات در ساختار داده به‌روزرسانی نمی‌شود. از طرف دیگر—و مهم‌تر—کامپیوتر اکنون می‌تواند آن مکان حافظه را برای مقاصد دیگر بازاستفاده کند! ممکن است بعداً داده‌هایی کاملاً نامرتبط بخوانید.

Two tables, depicting two futures, fut1 and fut2, each of which has one column and three rows, representing the result of having moved a future out of fut1 into fut2. The first, fut1, is grayed out, with a question mark in each index, representing unknown memory. The second, fut2, has 0 and 1 in the first and second rows and an arrow pointing from its third row back to the second row of fut1, representing a pointer that is referencing the old location in memory of the future before it was moved.
شکل ۱۷-۵: نتیجه ناایمن جابه‌جایی یک نوع داده که به خودش ارجاع دارد

از نظر تئوری، کامپایلر Rust می‌تواند سعی کند هر مرجع به یک شیء را هر زمان که جابه‌جا می‌شود، به‌روزرسانی کند، اما این کار می‌تواند سربار عملکرد زیادی ایجاد کند، به‌ویژه اگر یک شبکه کامل از مراجع نیاز به به‌روزرسانی داشته باشد. اگر بتوانیم به جای آن مطمئن شویم که ساختار داده مورد نظر در حافظه جابه‌جا نمی‌شود، نیازی به به‌روزرسانی مراجع نخواهیم داشت. این دقیقاً همان چیزی است که borrow checker در Rust نیاز دارد: در کد ایمن، از جابه‌جا کردن هر آیتمی که مرجع فعالی به آن دارد جلوگیری می‌کند.

Pin بر اساس این اصل عمل می‌کند و تضمین دقیقی که نیاز داریم را ارائه می‌دهد. وقتی یک مقدار را با بسته‌بندی یک اشاره‌گر به آن مقدار در Pin pin می‌کنیم، دیگر نمی‌تواند جابه‌جا شود. بنابراین، اگر Pin<Box<SomeType>> داشته باشید، در واقع مقدار SomeType را pin می‌کنید، نه اشاره‌گر Box. شکل ۱۷-۶ این فرآیند را نشان می‌دهد.

Three boxes laid out side by side. The first is labeled “Pin”, the second “b1”, and the third “pinned”. Within “pinned” is a table labeled “fut”, with a single column; it represents a future with cells for each part of the data structure. Its first cell has the value “0”, its second cell has an arrow coming out of it and pointing to the fourth and final cell, which has the value “1” in it, and the third cell has dashed lines and an ellipsis to indicate there may be other parts to the data structure. All together, the “fut” table represents a future which is self-referential. An arrow leaves the box labeled “Pin”, goes through the box labeled “b1” and has terminates inside the “pinned” box at the “fut” table.
شکل 17-6: pin کردن یک `Box` که به یک نوع آینده خودارجاعی اشاره می‌کند.

در واقع، اشاره‌گر (Pointer) Box هنوز می‌تواند به‌طور آزاد جابه‌جا شود. به یاد داشته باشید: ما به مطمئن شدن از اینکه داده‌ای که در نهایت به آن ارجاع داده می‌شود در جای خود باقی می‌ماند اهمیت می‌دهیم. اگر یک اشاره‌گر (Pointer) جابه‌جا شود اما داده‌ای که به آن اشاره می‌کند در همان مکان باقی بماند، همانطور که در شکل 17-7 نشان داده شده است، هیچ مشکلی پیش نمی‌آید. (چگونگی انجام این کار با یک Pin که یک Box را می‌پیچد فراتر از بحث این بخش خاص است، اما می‌تواند تمرین خوبی باشد! اگر به مستندات نوع‌ها و همچنین ماژول std::pin نگاه کنید، ممکن است بتوانید بفهمید چگونه این کار را انجام دهید.) نکته کلیدی این است که نوع خودارجاعی خود نمی‌تواند جابه‌جا شود، زیرا همچنان pin شده است.

Four boxes laid out in three rough columns, identical to the previous diagram with a change to the second column. Now there are two boxes in the second column, labeled “b1” and “b2”, “b1” is grayed out, and the arrow from “Pin” goes through “b2” instead of “b1”, indicating that the pointer has moved from “b1” to “b2”, but the data in “pinned” has not moved.
شکل 17-7: جابه‌جایی یک `Box` که به یک نوع آینده خودارجاعی اشاره می‌کند.

با این حال، اکثر انواع کاملاً برای جابه‌جایی ایمن هستند، حتی اگر پشت یک اشاره‌گر Pin قرار داشته باشند. فقط زمانی نیاز به فکر کردن به pinning داریم که آیتم‌ها دارای مراجع داخلی باشند. مقادیر اولیه (primitive) مانند اعداد و مقادیر Boolean ایمن هستند، زیرا به‌وضوح هیچ مرجع داخلی ندارند، بنابراین بدیهی است که ایمن هستند. بیشتر انواعی که معمولاً با آن‌ها در Rust کار می‌کنید نیز این‌گونه‌اند. برای مثال، می‌توانید یک Vec را بدون نگرانی جابه‌جا کنید. با توجه به آنچه تاکنون دیده‌ایم، اگر یک Pin<Vec<String>> داشته باشید، باید همه چیز را از طریق APIهای ایمن اما محدودکننده‌ای که Pin ارائه می‌دهد انجام دهید، حتی اگر یک Vec<String> همیشه برای جابه‌جایی ایمن باشد اگر هیچ مرجع دیگری به آن وجود نداشته باشد. ما به روشی نیاز داریم که به کامپایلر بگوییم در چنین مواردی جابه‌جا کردن آیتم‌ها مشکلی ندارد—و اینجا است که Unpin وارد عمل می‌شود.

Unpin یک ویژگی علامت‌گذار (marker trait) است، مشابه ویژگی‌های Send و Sync که در فصل ۱۶ دیدیم، و بنابراین هیچ عملکردی از خود ندارد. ویژگی‌های علامت‌گذار فقط برای این وجود دارند که به کامپایلر بگویند استفاده از نوعی که یک ویژگی خاص را پیاده‌سازی می‌کند در یک زمینه خاص ایمن است. Unpin به کامپایلر اطلاع می‌دهد که یک نوع خاص نیازی به تضمین اینکه مقدار مربوطه به‌صورت ایمن جابه‌جا می‌شود، ندارد.

مشابه Send و Sync، کامپایلر به‌طور خودکار Unpin را برای تمام انواعی که می‌تواند ثابت کند ایمن هستند، پیاده‌سازی می‌کند. یک مورد خاص، دوباره مشابه Send و Sync، این است که Unpin برای یک نوع پیاده‌سازی نمی‌شود. نشانه‌گذاری برای این حالت به شکل impl !Unpin for SomeType است، که در آن SomeType نام نوعی است که باید آن تضمین‌ها را برای ایمن بودن، هر زمان که اشاره‌گری به آن نوع در یک Pin استفاده می‌شود، حفظ کند.

به عبارت دیگر، دو نکته در مورد رابطه بین Pin و Unpin باید در نظر داشته باشید. اول، Unpin حالت “معمولی” است و !Unpin حالت خاص. دوم، اینکه آیا یک نوع ویژگی Unpin یا !Unpin را پیاده‌سازی می‌کند فقط زمانی اهمیت دارد که در حال استفاده از یک اشاره‌گر pin شده به آن نوع مانند Pin<&mut SomeType> باشید.

برای روشن‌تر کردن این موضوع، به یک String فکر کنید: این نوع دارای طول و کاراکترهای Unicode است که آن را تشکیل می‌دهند. ما می‌توانیم یک String را در Pin بسته‌بندی کنیم، همان‌طور که در شکل ۱۷-۸ دیده می‌شود. با این حال، String به طور خودکار ویژگی Unpin را پیاده‌سازی می‌کند، همان‌طور که بیشتر انواع دیگر در Rust این کار را انجام می‌دهند.

Concurrent work flow
شکل ۱۷-۸: Pin کردن یک `String`؛ خط نقطه‌چین نشان می‌دهد که `String` ویژگی `Unpin` را پیاده‌سازی می‌کند و بنابراین pin نشده است.

در نتیجه، می‌توانیم کارهایی انجام دهیم که اگر String ویژگی !Unpin را پیاده‌سازی می‌کرد غیرقانونی بود، مانند جایگزین کردن یک رشته با رشته‌ای دیگر در همان مکان حافظه، همان‌طور که در شکل ۱۷-۹ نشان داده شده است. این کار قرارداد Pin را نقض نمی‌کند، زیرا String هیچ مرجع داخلی ندارد که جابه‌جایی آن را ناایمن کند! این دقیقاً دلیلی است که ویژگی Unpin را به جای !Unpin پیاده‌سازی می‌کند.

Concurrent work flow
شکل 17-9: جایگزینی یک String با یک String کاملاً متفاوت در حافظه.

اکنون به‌اندازه کافی می‌دانیم تا خطاهایی که برای آن فراخوانی join_all در فهرست 17-17 گزارش شدند را درک کنیم. ما در ابتدا سعی کردیم آینده‌های تولیدشده توسط بلوک‌های async را به یک Vec<Box<dyn Future<Output = ()>>> منتقل کنیم، اما همان‌طور که دیدیم، این آینده‌ها ممکن است ارجاعات داخلی داشته باشند، بنابراین ویژگی Unpin را پیاده‌سازی نمی‌کنند. آن‌ها نیاز به pin شدن دارند، و سپس می‌توانیم نوع Pin را به Vec ارسال کنیم، با اطمینان از اینکه داده‌های زیربنایی در آینده‌ها جابه‌جا نخواهند شد.

Pin و Unpin بیشتر برای ساخت کتابخانه‌های سطح پایین یا وقتی که خودتان یک runtime می‌سازید مهم هستند، نه برای کد روزمره راست. وقتی این Traits را در پیام‌های خطا مشاهده می‌کنید، اکنون ایده بهتری از نحوه رفع کد خواهید داشت!

نکته: این ترکیب Pin و Unpin اجازه می‌دهد که یک کلاس کامل از نوع‌های پیچیده در راست ایمن باشند که در غیر این صورت به دلیل خودارجاعی بودن دشوار برای پیاده‌سازی هستند. نوع‌هایی که نیاز به Pin دارند بیشتر در راست async امروزی ظاهر می‌شوند، اما ممکن است—بسیار به‌ندرت!—در زمینه‌های دیگر نیز ببینید.

جزئیات نحوه کار Pin و Unpin و قوانینی که باید رعایت کنند، به‌طور گسترده در مستندات API برای std::pin پوشش داده شده‌اند، بنابراین اگر می‌خواهید آن‌ها را عمیق‌تر درک کنید، این مکان خوبی برای شروع است.

اگر می‌خواهید بفهمید که “در پشت صحنه” چگونه کار می‌کنند، کتاب رسمی برنامه‌نویسی ناهمگام در راست پاسخگوی شماست:

The Stream Trait

اکنون که درک عمیق‌تری از Traits‌های Future، Pin، و Unpin داریم، می‌توانیم توجه خود را به Trait Stream معطوف کنیم. همانطور که در بخش معرفی streams توضیح داده شد، streams مشابه iteratorهای ناهمگام هستند. برخلاف Iterator و Future، در زمان نگارش این متن، تعریف Stream در کتابخانه استاندارد وجود ندارد، اما یک تعریف بسیار رایج از crate futures وجود دارد که در سراسر اکوسیستم استفاده می‌شود.

بیایید تعاریف Traits‌های Iterator و Future را مرور کنیم تا بتوانیم تصور کنیم یک Trait Stream که این دو را ترکیب می‌کند چگونه ممکن است به نظر برسد. از Iterator، مفهوم یک توالی را داریم: متد next آن یک Option<Self::Item> فراهم می‌کند. از Future، مفهوم آماده شدن در طول زمان را داریم: متد poll آن یک Poll<Self::Output> فراهم می‌کند. برای نمایش یک توالی از آیتم‌هایی که در طول زمان آماده می‌شوند، یک Trait Stream تعریف می‌کنیم که این ویژگی‌ها را ترکیب می‌کند:

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;

    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>
    ) -> Poll<Option<Self::Item>>;
}
}

Trait Stream یک نوع مرتبط به نام Item برای نوع آیتم‌هایی که توسط stream تولید می‌شوند تعریف می‌کند. این مشابه با Iterator است: ممکن است تعداد این آیتم‌ها صفر تا بی‌نهایت باشد، برخلاف Future که همیشه یک Output واحد دارد (حتی اگر نوع واحد () باشد).

Stream همچنین یک متد برای دریافت این آیتم‌ها تعریف می‌کند. ما آن را poll_next می‌نامیم تا واضح باشد که این متد به همان روشی که Future::poll بررسی می‌کند، آیتم‌ها را بررسی می‌کند و به همان روشی که Iterator::next یک توالی از آیتم‌ها تولید می‌کند، آیتم‌ها را تولید می‌کند. نوع بازگشتی آن Poll را با Option ترکیب می‌کند. نوع خارجی Poll است، زیرا باید برای آماده بودن بررسی شود، همان‌طور که یک آینده بررسی می‌شود. نوع داخلی Option است، زیرا باید نشان دهد که آیا پیام‌های بیشتری وجود دارد یا نه، همان‌طور که یک iterator انجام می‌دهد.

چیزی بسیار مشابه با این احتمالاً در نهایت به‌عنوان بخشی از کتابخانه استاندارد راست استانداردسازی خواهد شد. در حال حاضر، این Trait بخشی از ابزار اکثر runtime‌ها است، بنابراین می‌توانید روی آن حساب کنید و همه چیزهایی که در ادامه می‌بینید عموماً قابل اعمال هستند!

با این حال، در مثالی که در بخش مربوط به streams دیدیم، ما از poll_next یا Stream استفاده نکردیم، بلکه از next و StreamExt استفاده کردیم. البته می‌توانیم مستقیماً از API poll_next استفاده کنیم و ماشین‌های حالت Stream خود را با دست بنویسیم، همان‌طور که می‌توانیم مستقیماً از طریق متد poll با آینده‌ها کار کنیم. اما استفاده از await بسیار دلپذیرتر است، بنابراین Trait StreamExt متد next را فراهم می‌کند تا بتوانیم دقیقاً این کار را انجام دهیم.

#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};

trait Stream {
    type Item;
    fn poll_next(
        self: Pin<&mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item>>;
}

trait StreamExt: Stream {
    async fn next(&mut self) -> Option<Self::Item>
    where
        Self: Unpin;

    // other methods...
}
}

نکته: تعریف واقعی که قبلاً در این فصل استفاده کردیم کمی متفاوت به نظر می‌رسد، زیرا از نسخه‌هایی از راست پشتیبانی می‌کند که هنوز از استفاده از توابع async در Traits پشتیبانی نمی‌کنند. در نتیجه، این‌گونه به نظر می‌رسد:

fn next(&mut self) -> Next<'_, Self> where Self: Unpin;

نوع Next یک struct است که Future را پیاده‌سازی می‌کند و راهی برای نام‌گذاری طول عمر ارجاع به self با Next<'_, Self> فراهم می‌کند، به‌طوری که await بتواند با این متد کار کند!

Trait StreamExt همچنین محل تمام متدهای جالبی است که می‌توان با streams استفاده کرد. StreamExt به‌طور خودکار برای هر نوعی که Stream را پیاده‌سازی کند، پیاده‌سازی می‌شود، اما این Traits به‌طور جداگانه تعریف شده‌اند تا جامعه بتواند به‌صورت جداگانه روی Trait بنیادی و API‌های راحتی کار کند.

در نسخه StreamExt استفاده‌شده در crate trpl، این Trait نه تنها متد next را تعریف می‌کند، بلکه یک پیاده‌سازی از next ارائه می‌دهد که جزئیات فراخوانی Stream::poll_next را به‌درستی مدیریت می‌کند. این بدان معناست که حتی زمانی که نیاز دارید نوع داده‌های جریان خود را بنویسید، فقط کافی است Stream را پیاده‌سازی کنید، و سپس هرکسی که از نوع داده شما استفاده کند، می‌تواند به‌طور خودکار از StreamExt و متدهای آن با آن استفاده کند.

این تمام چیزی است که درباره جزئیات سطح پایین این Traits پوشش خواهیم داد. برای جمع‌بندی، بیایید در نظر بگیریم که چگونه آینده‌ها (شامل streams)، تسک‌ها، و نخ‌ها همگی با هم سازگار هستند!