পাঠ ১৭.৫

Async-এর Trait-গুলোতে আরও গভীরে

A Closer Look at the Traits for Async

এতক্ষণ আমরা async/.await ব্যবহার করেছি; এবার দেখা যাক আড়ালে যে trait-গুলো কাজ করছে — Future, Pin, Unpin, এবং Stream। দৈনন্দিন code-এ সরাসরি ব্যবহার কম, কিন্তু error message বুঝতে এবং custom abstraction লিখতে এরা জানা দরকার।

Future trait

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>;
}
  • Output — associated type; future ready হলে এই type-এর value দেবে।
  • poll — runtime এই method call করে জানতে চায় future ready হয়েছে কিনা।
  • Self Pin<&mut Self> — কেন pinning দরকার, পরে বলব।
  • Context — runtime-এর সাথে communicate করার channel (যেমন waker)।

Poll enum

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

Pending মানে "এখনো ready না, পরে আবার call করো"; Ready(T) মানে "শেষ, এই value নাও"। সাধারণ rule — Ready return করার পর আবার poll call করতে নেই (যদি না documentation explicitly বলে)।

await আসলে কী হয়

Compiler .await-কে conceptually এমন code-এ transform করে:

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

আসল code-এ runtime এই loop-টাই চালায় — busy loop নয়, waker দিয়ে notification system — এবং Pending হলে other future-কে চলার সুযোগ দেয়।

Pin এবং Unpin

Async block থেকে compile হওয়া future-গুলো কখনো কখনো নিজের ভিতরের data-কে refer করে (self-referential)। যেমন একটা local variable-এর reference পরের state-এ রাখা। এমন data memory-তে move হলে reference invalid হয়ে যাবে।

Pin — pointer type-এর (&, &mut, Box, Rc) wrapper, যা wrapped data-কে memory-তে move হতে দেয় না

Pin<Box<SomeType>>  // Pins the SomeType value, not the Box pointer

লক্ষ্য — Box pointer-টা যেকোনো জায়গায় move হতে পারে; কিন্তু এটা যা point করে আছে সেটা স্থির থাকে।

Unpin marker

UnpinSend/Sync এর মতো একটা marker trait। বলে — "এই type pin করা সত্ত্বেও move করা safe।"

  • অধিকাংশ type automatic Unpin implement করে — যেমন String, Vec
  • Self-referential structure-গুলো !Unpin — অর্থাৎ Unpin না।
  • Unpin normal case; !Unpin special।
  • এর প্রয়োজন শুধু Pin<&mut T> ব্যবহার করার সময়।

কোথায় এটা practical-এ আসে

আগের পাঠের multiple-producer code-এ যদি future-গুলো Vec<Box<dyn Future>>-এ রাখতে চাও, error আসবে — কারণ dyn Future Unpin implement করে না।

let futures: Vec<Box<dyn Future<Output = ()>>> =
    vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];

trpl::join_all(futures).await;  // Error: dyn Future doesn't implement Unpin

সমাধান — pin! macro দিয়ে future-গুলো pin করে, type-টা Pin<&mut dyn Future> বানাও:

use std::pin::{Pin, pin};

let tx1_fut = pin!(async move {
    let vals = vec![
        String::from("hi"),
        String::from("from"),
        String::from("the"),
        String::from("future"),
    ];

    for val in vals {
        tx1.send(val).unwrap();
        trpl::sleep(Duration::from_secs(1)).await;
    }
});

let rx_fut = pin!(async {
    while let Some(value) = rx.recv().await {
        println!("received '{value}'");
    }
});

let tx_fut = pin!(async move {
    let vals = vec![
        String::from("more"),
        String::from("messages"),
        String::from("for"),
        String::from("you"),
    ];

    for val in vals {
        tx.send(val).unwrap();
        trpl::sleep(Duration::from_secs(1)).await;
    }
});

let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
    vec![tx1_fut, rx_fut, tx_fut];

trpl::join_all(futures).await;

এখন future-গুলো pinned, আর join_all খুশি।

Stream trait

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>>;
}

লক্ষ্য করো — Future-এর poll Poll<Output> দেয়, Stream-এর poll_next Poll<Option<Item>> দেয় — অর্থাৎ Future-এর Poll + Iterator-এর OptionNone এলে stream শেষ।

StreamExt

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

    // other methods...
}

StreamExtStream-কে extend করে next ও অন্যান্য utility method দেয় যাতে আমাদের সরাসরি poll_next call করতে না হয়। Stream implement করলে StreamExt automatic — separation-এর সুবিধা হলো community নতুন utility method add করতে পারে base trait না বদলে।

Quick reference table

  • Future — single async value; poll → Poll<Output>
  • Stream — async value-এর sequence; poll_next → Poll<Option<Item>>
  • Pin — wrapper, wrapped data-কে move হতে দেয় না।
  • Unpin — marker; pin করা সত্ত্বেও move safe।

এই পাঠ থেকে যা শিখলে

  • Future trait-এ poll method এবং Poll<T> enum (Ready/Pending) — .await-এর foundation।
  • Async block থেকে generated future self-referential হতে পারে — তাই memory-তে move হতে পারে না।
  • Pin data-কে memory-তে স্থির রাখে; Unpin marker trait যেটা বলে "move safe"।
  • pin! macro দিয়ে future pin করে collection-এ রাখা যায়।
  • Stream = Future + Iterator; StreamExt next-এর মতো utility add করে।