অ্যাসিঙ্ক্রোনাসের জন্য ব্যবহৃত Trait গুলির আরও গভীর পর্যালোচনা (A Closer Look at the Traits for Async)

এই চ্যাপ্টার জুড়ে, আমরা Future, Pin, Unpin, Stream, এবং StreamExt trait গুলিকে বিভিন্ন উপায়ে ব্যবহার করেছি। এখনও পর্যন্ত, যদিও, আমরা কীভাবে সেগুলি কাজ করে বা কীভাবে সেগুলি একসাথে ফিট করে তার বিশদ বিবরণে খুব বেশি যাওয়া এড়িয়ে গেছি, যা আপনার প্রতিদিনের Rust-এর কাজের জন্য বেশিরভাগ সময় ঠিক আছে। কখনও কখনও, যদিও, আপনি এমন পরিস্থিতির সম্মুখীন হবেন যেখানে আপনাকে এই বিশদগুলির আরও কয়েকটি বুঝতে হবে। এই বিভাগে, আমরা সেই পরিস্থিতিতে সাহায্য করার জন্য যথেষ্ট গভীরে যাব, এখনও অন্যান্য ডকুমেন্টেশনের জন্য সত্যিই গভীর ডাইভ ছেড়ে দেব।

Future Trait

আসুন Future trait কীভাবে কাজ করে তা ঘনিষ্ঠভাবে দেখে শুরু করি। 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 সংজ্ঞায় অনেকগুলি নতুন টাইপ এবং কিছু সিনট্যাক্স রয়েছে যা আমরা আগে দেখিনি, তাই আসুন সংজ্ঞাটি একে একে দেখি।

প্রথমত, Future-এর অ্যাসোসিয়েটেড টাইপ Output বলে যে ফিউচারটি কী-তে রেজলভ করে। এটি Iterator trait-এর জন্য Item অ্যাসোসিয়েটেড টাইপের অনুরূপ। দ্বিতীয়ত, Future-এর poll মেথডও রয়েছে, যা তার self প্যারামিটারের জন্য একটি বিশেষ Pin রেফারেন্স এবং একটি Context টাইপের মিউটেবল রেফারেন্স নেয় এবং একটি Poll<Self::Output> রিটার্ন করে। আমরা একটু পরেই Pin এবং Context সম্পর্কে আরও কথা বলব। আপাতত, আসুন মেথডটি কী রিটার্ন করে, Poll টাইপ, সেদিকে মনোযোগ দিই:

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

এই Poll টাইপটি একটি Option-এর মতো। এটির একটি ভেরিয়েন্ট রয়েছে যার একটি মান রয়েছে, Ready(T), এবং একটি যার নেই, PendingPoll মানে Option-এর থেকে বেশ ভিন্ন কিছু! Pending ভেরিয়েন্ট নির্দেশ করে যে ফিউচারের এখনও কাজ করার আছে, তাই কলারকে পরে আবার পরীক্ষা করতে হবে। Ready ভেরিয়েন্ট নির্দেশ করে যে ফিউচারটি তার কাজ শেষ করেছে এবং T মানটি উপলব্ধ।

Note: বেশিরভাগ ফিউচারের সাথে, ফিউচারটি Ready রিটার্ন করার পরে কলারের আবার poll কল করা উচিত নয়। অনেক ফিউচার প্রস্তুত হওয়ার পরে আবার পোল করা হলে প্যানিক করবে। যে ফিউচারগুলি আবার পোল করা নিরাপদ সেগুলি তাদের ডকুমেন্টেশনে স্পষ্টভাবে তা বলবে। এটি Iterator::next কীভাবে আচরণ করে তার অনুরূপ।

যখন আপনি এমন কোড দেখেন যা await ব্যবহার করে, তখন Rust এটিকে হুডের নিচে poll কল করা কোডে কম্পাইল করে। আপনি যদি Listing 17-4-এ ফিরে তাকান, যেখানে আমরা একটি একক 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?
    }
}

ফিউচারটি এখনও Pending হলে আমাদের কী করা উচিত? আমাদের আবার, এবং আবার, এবং আবার চেষ্টা করার কিছু উপায় দরকার, যতক্ষণ না ফিউচারটি অবশেষে প্রস্তুত হয়। অন্য কথায়, আমাদের একটি লুপ দরকার:

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 ব্লকিং হত—ঠিক আমরা যা করতে যাচ্ছিলাম তার বিপরীত! পরিবর্তে, Rust নিশ্চিত করে যে লুপটি এমন কিছুতে কন্ট্রোল হস্তান্তর করতে পারে যা এই ফিউচারে কাজ করা বন্ধ করে অন্য ফিউচারগুলিতে কাজ করতে পারে এবং তারপর এটি আবার পরে পরীক্ষা করতে পারে। যেমনটি আমরা দেখেছি, সেই কিছু হল একটি অ্যাসিঙ্ক্রোনাস রানটাইম এবং এই শিডিউলিং এবং সমন্বয় কাজ হল এর প্রধান কাজগুলির মধ্যে একটি।

এই চ্যাপ্টারের শুরুতে, আমরা rx.recv-এর জন্য অপেক্ষা করার বর্ণনা দিয়েছি। recv কলটি একটি ফিউচার রিটার্ন করে এবং ফিউচারের জন্য অপেক্ষা করা এটিকে পোল করে। আমরা উল্লেখ করেছি যে একটি রানটাইম ফিউচারটিকে ততক্ষণ পর্যন্ত থামিয়ে রাখবে যতক্ষণ না এটি হয় Some(message) অথবা চ্যানেলটি বন্ধ হয়ে গেলে None দিয়ে প্রস্তুত হয়। Future trait এবং বিশেষ করে Future::poll সম্পর্কে আমাদের গভীর উপলব্ধির সাথে, আমরা দেখতে পাচ্ছি কীভাবে এটি কাজ করে। রানটাইম জানে যে ফিউচারটি প্রস্তুত নয় যখন এটি Poll::Pending রিটার্ন করে। বিপরীতভাবে, রানটাইম জানে যে ফিউচারটি প্রস্তুত এবং poll যখন Poll::Ready(Some(message)) বা Poll::Ready(None) রিটার্ন করে তখন এটিকে অগ্রসর করে।

একটি রানটাইম কীভাবে এটি করে তার সঠিক বিবরণ এই বইয়ের সুযোগের বাইরে, তবে মূল বিষয় হল ফিউচারের বেসিক মেকানিক্স দেখা: একটি রানটাইম প্রতিটি ফিউচারকে পোল করে যার জন্য এটি দায়ী, ফিউচারটি এখনও প্রস্তুত না হলে এটিকে আবার ঘুমাতে ফিরিয়ে দেয়।

Pin এবং Unpin Trait

আমরা যখন Listing 17-16-এ পিনিং-এর ধারণাটি চালু করি, তখন আমরা একটি খুব জটিল error মেসেজের সম্মুখীন হয়েছিলাম। এখানে এটির প্রাসঙ্গিক অংশটি আবার রয়েছে:

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}`
   |
   = 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-1949cf8c6b5b557f/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`

এই error মেসেজটি আমাদের কেবল বলে না যে আমাদের মানগুলিকে পিন করতে হবে, তবে পিনিং কেন প্রয়োজন তাও বলে। trpl::join_all ফাংশনটি JoinAll নামক একটি স্ট্রাক্ট রিটার্ন করে। সেই স্ট্রাক্টটি একটি টাইপ F-এর উপর জেনেরিক, যা Future trait ইমপ্লিমেন্ট করার জন্য সীমাবদ্ধ। একটি ফিউচারকে সরাসরি await দিয়ে অপেক্ষা করা ফিউচারটিকে অন্তর্নিহিতভাবে পিন করে। তাই আমরা যেখানেই ফিউচারের জন্য অপেক্ষা করতে চাই সেখানেই আমাদের pin! ব্যবহার করার প্রয়োজন নেই।

যাইহোক, আমরা এখানে সরাসরি একটি ফিউচারের জন্য অপেক্ষা করছি না। পরিবর্তে, আমরা join_all ফাংশনে ফিউচারের একটি কালেকশন পাস করে একটি নতুন ফিউচার, JoinAll তৈরি করি। join_all-এর স্বাক্ষরের জন্য কালেকশনের আইটেমগুলির টাইপগুলির প্রত্যেককেই Future trait ইমপ্লিমেন্ট করতে হবে এবং Box<T> শুধুমাত্র তখনই Future ইমপ্লিমেন্ট করে যদি T যেটি এটিকে র‍্যাপ করে সেটি একটি ফিউচার হয় যা Unpin trait ইমপ্লিমেন্ট করে।

এটি হজম করার জন্য অনেক কিছু! এটিকে সত্যিই বোঝার জন্য, আসুন Future trait কীভাবে কাজ করে, বিশেষ করে পিনিং-এর চারপাশে আরও একটু গভীরে ডুব দিই।

Future trait-এর সংজ্ঞাটি আবার দেখুন:

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

pub trait Future {
    type Output;

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

cx প্যারামিটার এবং এর Context টাইপ হল কীভাবে একটি রানটাইম আসলে জানে কখন কোনও প্রদত্ত ফিউচার পরীক্ষা করতে হবে এবং এখনও অলস থাকতে হবে তার মূল বিষয়। আবারও, এটি কীভাবে কাজ করে তার বিশদ বিবরণ এই চ্যাপ্টারের সুযোগের বাইরে এবং আপনি সাধারণত শুধুমাত্র তখনই এটি সম্পর্কে চিন্তা করতে হবে যখন আপনি একটি কাস্টম Future ইমপ্লিমেন্টেশন লিখছেন। পরিবর্তে আমরা self-এর জন্য টাইপের উপর ফোকাস করব, কারণ এটি প্রথমবার যখন আমরা একটি মেথড দেখেছি যেখানে self-এর একটি টাইপ অ্যানোটেশন রয়েছে। self-এর জন্য একটি টাইপ অ্যানোটেশন অন্যান্য ফাংশন প্যারামিটারের জন্য টাইপ অ্যানোটেশনের মতোই কাজ করে, তবে দুটি মূল পার্থক্য সহ:

  • এটি Rust-কে বলে যে মেথডটি কল করার জন্য self অবশ্যই কোন টাইপের হতে হবে।

  • এটি কেবল যেকোনো টাইপ হতে পারে না। এটি যে টাইপের উপর মেথডটি ইমপ্লিমেন্ট করা হয়েছে, সেই টাইপের একটি রেফারেন্স বা স্মার্ট পয়েন্টার, অথবা সেই টাইপের একটি রেফারেন্স র‍্যাপ করা একটি Pin-এ সীমাবদ্ধ।

আমরা Chapter 18-এ এই সিনট্যাক্স সম্পর্কে আরও দেখব। আপাতত, এটি জানাই যথেষ্ট যে আমরা যদি একটি ফিউচারকে পোল করতে চাই এটি Pending নাকি Ready(Output) তা পরীক্ষা করার জন্য, তাহলে আমাদের টাইপের একটি Pin-এ র‍্যাপ করা মিউটেবল রেফারেন্স দরকার।

Pin হল পয়েন্টার-জাতীয় টাইপ যেমন &, &mut, Box, এবং Rc-এর জন্য একটি র‍্যাপার। (টেকনিক্যালি, Pin এমন টাইপের সাথে কাজ করে যা Deref বা DerefMut trait ইমপ্লিমেন্ট করে, কিন্তু এটি কার্যকরভাবে শুধুমাত্র পয়েন্টারগুলির সাথে কাজ করার সমতুল্য।) Pin নিজে কোনও পয়েন্টার নয় এবং Rc এবং Arc-এর রেফারেন্স কাউন্টিংয়ের সাথে যেমন আচরণ করে তেমন কোনও নিজস্ব আচরণ নেই; এটি সম্পূর্ণরূপে একটি টুল যা কম্পাইলার পয়েন্টার ব্যবহারের উপর সীমাবদ্ধতা প্রয়োগ করতে ব্যবহার করতে পারে।

await কে poll-এ কলের পরিপ্রেক্ষিতে ইমপ্লিমেন্ট করা হয় তা স্মরণ করা আমাদের আগে দেখা error মেসেজটি ব্যাখ্যা করতে শুরু করে, কিন্তু সেটি Unpin-এর পরিপ্রেক্ষিতে ছিল, Pin-এর নয়। তাহলে Pin কীভাবে Unpin-এর সাথে সম্পর্কিত এবং Future-এর poll কল করার জন্য self-কে একটি Pin টাইপে থাকা কেন প্রয়োজন?

এই চ্যাপ্টারের শুরুতে মনে করুন একটি ফিউচারের অ্যাওয়েট পয়েন্টগুলির একটি সিরিজ একটি স্টেট মেশিনে কম্পাইল করা হয় এবং কম্পাইলার নিশ্চিত করে যে স্টেট মেশিনটি borrowing এবং ownership সহ Rust-এর নিরাপত্তার চারপাশে সমস্ত স্বাভাবিক নিয়ম অনুসরণ করে। এটি কাজ করার জন্য, Rust দেখে যে একটি অ্যাওয়েট পয়েন্ট এবং হয় পরবর্তী অ্যাওয়েট পয়েন্ট বা অ্যাসিঙ্ক্রোনাস ব্লকের শেষের মধ্যে কোন ডেটার প্রয়োজন। তারপর এটি কম্পাইল করা স্টেট মেশিনে একটি সংশ্লিষ্ট ভেরিয়েন্ট তৈরি করে। প্রতিটি ভেরিয়েন্ট সোর্স কোডের সেই বিভাগে ব্যবহৃত ডেটাতে তার প্রয়োজনীয় অ্যাক্সেস পায়, হয় সেই ডেটার ownership নিয়ে বা সেটিতে একটি মিউটেবল বা ইমিউটেবল রেফারেন্স পেয়ে।

এখন পর্যন্ত, সবকিছু ঠিক আছে: আমরা যদি একটি প্রদত্ত অ্যাসিঙ্ক্রোনাস ব্লকে ownership বা রেফারেন্স সম্পর্কে কিছু ভুল করি, তাহলে borrow চেকার আমাদের বলবে। যখন আমরা সেই ব্লকের সাথে সম্পর্কিত ফিউচারটিকে সরাতে চাই—যেমন এটিকে join_all-এর সাথে ব্যবহার করার জন্য একটি Vec-এ সরানো—জিনিসগুলি আরও কঠিন হয়ে যায়।

যখন আমরা একটি ফিউচারকে সরিয়ে দিই—সেটিকে join_all-এর সাথে ব্যবহার করার জন্য একটি ডেটা স্ট্রাকচারে পুশ করে বা একটি ফাংশন থেকে রিটার্ন করে—তার মানে আসলে আমাদের জন্য Rust যে স্টেট মেশিন তৈরি করে তা সরানো। এবং Rust-এ অন্য বেশিরভাগ টাইপের বিপরীতে, অ্যাসিঙ্ক্রোনাস ব্লকের জন্য Rust যে ফিউচারগুলি তৈরি করে সেগুলি কোনও প্রদত্ত ভেরিয়েন্টের ফিল্ডে নিজেদের রেফারেন্স সহ শেষ হতে পারে, যেমনটি চিত্র 17-4-এ সরলীকৃত চিত্রে দেখানো হয়েছে।

একটি একক-কলাম, তিনটি সারির টেবিল একটি ফিউচার, fut1-কে উপস্থাপন করে, যার প্রথম দুটি সারিতে 0 এবং 1 ডেটা মান রয়েছে এবং তৃতীয় সারি থেকে দ্বিতীয় সারিতে ফিরে আসা একটি তীর রয়েছে, যা ফিউচারের মধ্যে একটি অভ্যন্তরীণ রেফারেন্সকে উপস্থাপন করে।
চিত্র 17-4: একটি সেলফ-রেফারেন্সিয়াল ডেটা টাইপ।

ডিফল্টভাবে, যদিও, যে কোনও অবজেক্ট যার নিজের প্রতি রেফারেন্স রয়েছে সেটি সরানো অনিরাপদ, কারণ রেফারেন্সগুলি সর্বদা তারা যেটিকে রেফার করে তার প্রকৃত মেমরি অ্যাড্রেসের দিকে নির্দেশ করে (চিত্র 17-5 দেখুন)। আপনি যদি ডেটা স্ট্রাকচারটি নিজেই সরিয়ে দেন, তাহলে সেই অভ্যন্তরীণ রেফারেন্সগুলি পুরানো লোকেশনের দিকে নির্দেশ করে থাকবে। যাইহোক, সেই মেমরি লোকেশনটি এখন অবৈধ। একটির জন্য, আপনি যখন ডেটা স্ট্রাকচারে পরিবর্তন করবেন তখন এর মান আপডেট করা হবে না। অন্য—আরও গুরুত্বপূর্ণ—জিনিসের জন্য, কম্পিউটার এখন অন্য উদ্দেশ্যে সেই মেমরিটি পুনরায় ব্যবহার করতে মুক্ত! আপনি পরে সম্পূর্ণ সম্পর্কহীন ডেটা পড়তে পারেন।

দুটি টেবিল, দুটি ফিউচার, fut1 এবং fut2-কে চিত্রিত করে, যার প্রত্যেকটিতে একটি কলাম এবং তিনটি সারি রয়েছে, যা fut1 থেকে fut2-তে একটি ফিউচার সরানোর ফলাফলকে উপস্থাপন করে। প্রথমটি, fut1, ধূসর হয়ে গেছে, প্রতিটি ইনডেক্সে একটি প্রশ্ন চিহ্ন সহ, অজানা মেমরি উপস্থাপন করে। দ্বিতীয়টি, fut2-এর প্রথম এবং দ্বিতীয় সারিতে 0 এবং 1 রয়েছে এবং এর তৃতীয় সারি থেকে fut1-এর দ্বিতীয় সারিতে ফিরে আসা একটি তীর রয়েছে, যা একটি পয়েন্টারকে উপস্থাপন করে যা ফিউচারটি সরানোর আগে মেমরিতে পুরানো লোকেশনটিকে রেফারেন্স করছে।
চিত্র 17-5: একটি সেলফ-রেফারেন্সিয়াল ডেটা টাইপ সরানোর অনিরাপদ ফলাফল

তাত্ত্বিকভাবে, Rust কম্পাইলার যখনই কোনও অবজেক্ট সরানো হয় তখনই সেটিতে প্রতিটি রেফারেন্স আপডেট করার চেষ্টা করতে পারে, তবে এটি প্রচুর পারফরম্যান্স ওভারহেড যুক্ত করতে পারে, বিশেষ করে যদি রেফারেন্সের একটি সম্পূর্ণ ওয়েব আপডেট করার প্রয়োজন হয়। পরিবর্তে আমরা যদি নিশ্চিত করতে পারি যে প্রশ্নে থাকা ডেটা স্ট্রাকচারটি মেমরিতে সরানো হচ্ছে না, তাহলে আমাদের কোনও রেফারেন্স আপডেট করতে হবে না। এটিই Rust-এর borrow চেকারের প্রয়োজন: নিরাপদ কোডে, এটি আপনাকে কোনও আইটেমকে সক্রিয় রেফারেন্স সহ সরানোর অনুমতি দেয় না।

Pin আমাদের প্রয়োজনীয় গ্যারান্টি দেওয়ার জন্য এটির উপর ভিত্তি করে তৈরি। যখন আমরা একটি মানকে পিন করি সেই মানের একটি পয়েন্টারকে Pin-এ র‍্যাপ করে, তখন এটি আর সরতে পারে না। সুতরাং, যদি আপনার কাছে Pin<Box<SomeType>> থাকে, তাহলে আপনি আসলে SomeType মানটিকে পিন করেন, Box পয়েন্টারকে নয়। চিত্র 17-6 এই প্রক্রিয়াটি চিত্রিত করে।

পাশাপাশি তিনটি বাক্স রাখা হয়েছে। প্রথমটি “Pin” লেবেলযুক্ত, দ্বিতীয়টি “b1” এবং তৃতীয়টি “pinned”। “pinned”-এর মধ্যে “fut” লেবেলযুক্ত একটি টেবিল রয়েছে, একটি একক কলাম সহ; এটি ডেটা স্ট্রাকচারের প্রতিটি অংশের জন্য সেল সহ একটি ফিউচারকে উপস্থাপন করে। এর প্রথম সেলটিতে “0” মান রয়েছে, এর দ্বিতীয় সেলটিতে একটি তীর বেরিয়ে এসেছে এবং চতুর্থ এবং চূড়ান্ত সেলের দিকে নির্দেশ করছে, যেখানে “1” মান রয়েছে এবং তৃতীয় সেলটিতে ড্যাশযুক্ত লাইন এবং একটি এলিপসিস রয়েছে যা নির্দেশ করে যে ডেটা স্ট্রাকচারের অন্যান্য অংশ থাকতে পারে। সব মিলিয়ে, “fut” টেবিলটি একটি ফিউচারকে উপস্থাপন করে যা সেলফ-রেফারেন্সিয়াল। একটি তীর “Pin” লেবেলযুক্ত বাক্সটি ছেড়ে যায়, “b1” লেবেলযুক্ত বাক্সের মধ্য দিয়ে যায় এবং “fut” টেবিলে “pinned” বাক্সের ভিতরে শেষ হয়।
চিত্র 17-6: একটি `Box` পিন করা যা একটি সেলফ-রেফারেন্সিয়াল ফিউচার টাইপের দিকে নির্দেশ করে।

প্রকৃতপক্ষে, Box পয়েন্টারটি এখনও অবাধে ঘোরাফেরা করতে পারে। মনে রাখবেন: আমরা নিশ্চিত করতে চাই যে চূড়ান্তভাবে রেফারেন্স করা ডেটা যথাস্থানে রয়েছে। যদি একটি পয়েন্টার ঘোরাফেরা করে, কিন্তু এটি যে ডেটার দিকে নির্দেশ করে সেটি একই জায়গায় থাকে, যেমনটি চিত্র 17-7-এ রয়েছে, তাহলে কোনও সম্ভাব্য সমস্যা নেই। একটি স্বাধীন অনুশীলন হিসাবে, টাইপগুলির জন্য ডক্স এবং সেইসাথে std::pin মডিউলটি দেখুন এবং Pin র‍্যাপ করা একটি Box-এর সাথে আপনি এটি কীভাবে করবেন তা বের করার চেষ্টা করুন।) মূল বিষয় হল সেলফ-রেফারেন্সিয়াল টাইপটি নিজেই সরতে পারে না, কারণ এটি এখনও পিন করা আছে।

তিনটি কলামে চারটি বাক্স সাজানো হয়েছে, যা দ্বিতীয় কলামের পরিবর্তনের সাথে আগের ডায়াগ্রামের মতোই। এখন দ্বিতীয় কলামে “b1” এবং “b2” লেবেলযুক্ত দুটি বাক্স রয়েছে, “b1” ধূসর হয়ে গেছে এবং “Pin” থেকে তীরটি “b1”-এর পরিবর্তে “b2”-এর মধ্য দিয়ে যায়, যা নির্দেশ করে যে পয়েন্টারটি “b1” থেকে “b2”-তে সরে গেছে, কিন্তু “pinned”-এর ডেটা সরেনি।
চিত্র 17-7: একটি `Box` সরানো যা একটি সেলফ-রেফারেন্সিয়াল ফিউচার টাইপের দিকে নির্দেশ করে।

যাইহোক, বেশিরভাগ টাইপ ঘোরাফেরা করা সম্পূর্ণ নিরাপদ, এমনকি যদি সেগুলি একটি Pin র‍্যাপারের পিছনে থাকে। আইটেমগুলির অভ্যন্তরীণ রেফারেন্স থাকলে আমাদের কেবল পিনিং সম্পর্কে চিন্তা করতে হবে। সংখ্যা এবং বুলিয়ানের মতো প্রিমিটিভ মানগুলিতে স্পষ্টতই কোনও অভ্যন্তরীণ রেফারেন্স নেই, তাই সেগুলি নিরাপদ। Rust-এ আপনি সাধারণত যেগুলির সাথে কাজ করেন তার বেশিরভাগ টাইপও নিরাপদ। উদাহরণস্বরূপ, আপনি কোনও চিন্তা ছাড়াই একটি Vec ঘোরাফেরা করতে পারেন। শুধুমাত্র আমরা যা দেখেছি তা দেওয়া হলে, যদি আপনার কাছে একটি Pin<Vec<String>> থাকে, তাহলে আপনাকে Pin দ্বারা প্রদত্ত নিরাপদ কিন্তু সীমাবদ্ধ API-গুলির মাধ্যমে সবকিছু করতে হবে, যদিও একটি Vec<String> সর্বদা সরানো নিরাপদ যদি সেটিতে অন্য কোনও রেফারেন্স না থাকে। আমাদের কম্পাইলারকে বলার একটি উপায় দরকার যে এই ধরনের ক্ষেত্রে আইটেমগুলিকে ঘোরাফেরা করা ঠিক আছে—এবং সেখানেই Unpin কার্যকর হয়।

Unpin হল একটি মার্কার trait, Chapter 16-এ আমরা যে Send এবং Sync trait গুলি দেখেছি তার মতোই এবং এইভাবে এর নিজস্ব কোনও কার্যকারিতা নেই। মার্কার trait গুলি শুধুমাত্র কম্পাইলারকে বলার জন্য বিদ্যমান যে একটি প্রদত্ত trait ইমপ্লিমেন্ট করা টাইপটি একটি নির্দিষ্ট প্রসঙ্গে ব্যবহার করা নিরাপদ। Unpin কম্পাইলারকে জানায় যে একটি প্রদত্ত টাইপ প্রশ্নে থাকা মানটি নিরাপদে সরানো যেতে পারে কিনা সে সম্পর্কে কোনও গ্যারান্টি বহাল রাখার প্রয়োজন নেই

Send এবং Sync-এর মতোই, কম্পাইলার স্বয়ংক্রিয়ভাবে সমস্ত টাইপের জন্য Unpin ইমপ্লিমেন্ট করে যেখানে এটি প্রমাণ করতে পারে যে এটি নিরাপদ। আবারও Send এবং Sync-এর মতো একটি বিশেষ ক্ষেত্র হল যেখানে একটি টাইপের জন্য Unpin ইমপ্লিমেন্ট করা হয় না। এর জন্য নোটেশন হল impl !Unpin for SomeType, যেখানে SomeType হল এমন একটি টাইপের নাম যা প্রয়োজন সেই গ্যারান্টিগুলি বহাল রাখা নিরাপদ হতে যখন সেই টাইপের একটি পয়েন্টার একটি Pin-এ ব্যবহার করা হয়।

অন্য কথায়, Pin এবং Unpin-এর মধ্যে সম্পর্ক সম্পর্কে মনে রাখার মতো দুটি জিনিস রয়েছে। প্রথমত, Unpin হল “স্বাভাবিক” ক্ষেত্র এবং !Unpin হল বিশেষ ক্ষেত্র। দ্বিতীয়ত, একটি টাইপ Unpin ইমপ্লিমেন্ট করে নাকি !Unpin শুধুমাত্র তখনই গুরুত্বপূর্ণ যখন আপনি সেই টাইপের একটি পিন করা পয়েন্টার ব্যবহার করছেন যেমন Pin<&mut SomeType>

এটিকে কংক্রিট করতে, একটি String সম্পর্কে চিন্তা করুন: এটির একটি দৈর্ঘ্য এবং ইউনিকোড অক্ষর রয়েছে যা এটিকে তৈরি করে। আমরা একটি String কে Pin-এ র‍্যাপ করতে পারি, যেমনটি চিত্র 17-8-এ দেখা গেছে। যাইহোক, String স্বয়ংক্রিয়ভাবে Unpin ইমপ্লিমেন্ট করে, যেমনটি Rust-এর বেশিরভাগ অন্যান্য টাইপ করে।

কনকারেন্ট ওয়ার্ক ফ্লো
চিত্র 17-8: একটি `String` পিন করা; ডটেড লাইন নির্দেশ করে যে `String` `Unpin` trait ইমপ্লিমেন্ট করে এবং এইভাবে পিন করা হয় না।

ফলস্বরূপ, আমরা এমন কিছু করতে পারি যা অবৈধ হবে যদি String পরিবর্তে !Unpin ইমপ্লিমেন্ট করত, যেমন মেমরিতে ঠিক একই স্থানে একটি স্ট্রিংকে অন্য একটি দিয়ে প্রতিস্থাপন করা যেমনটি চিত্র 17-9-এ রয়েছে। এটি Pin চুক্তি লঙ্ঘন করে না, কারণ String-এর কোনও অভ্যন্তরীণ রেফারেন্স নেই যা এটিকে ঘোরাফেরা করা অনিরাপদ করে তোলে! ঠিক এই কারণেই এটি !Unpin-এর পরিবর্তে Unpin ইমপ্লিমেন্ট করে।

কনকারেন্ট ওয়ার্ক ফ্লো
চিত্র 17-9: `String`-কে মেমরিতে একটি সম্পূর্ণ ভিন্ন `String` দিয়ে প্রতিস্থাপন করা।

এখন আমরা Listing 17-17 থেকে সেই join_all কলের জন্য রিপোর্ট করা error গুলি বোঝার জন্য যথেষ্ট জানি। আমরা মূলত অ্যাসিঙ্ক্রোনাস ব্লক দ্বারা উৎপাদিত ফিউচারগুলিকে একটি Vec<Box<dyn Future<Output = ()>>>-এ সরানোর চেষ্টা করেছি, কিন্তু যেমনটি আমরা দেখেছি, সেই ফিউচারগুলিতে অভ্যন্তরীণ রেফারেন্স থাকতে পারে, তাই সেগুলি Unpin ইমপ্লিমেন্ট করে না। তাদের পিন করা দরকার এবং তারপর আমরা Pin টাইপটিকে Vec-এ পাস করতে পারি, এই আত্মবিশ্বাসের সাথে যে ফিউচারের অন্তর্নিহিত ডেটা সরানো হবে না

Pin এবং Unpin বেশিরভাগ ক্ষেত্রে নিম্ন-স্তরের লাইব্রেরি তৈরি করার জন্য গুরুত্বপূর্ণ, অথবা যখন আপনি নিজেই একটি রানটাইম তৈরি করছেন, প্রতিদিনের Rust কোডের জন্য নয়। যখন আপনি error মেসেজে এই trait গুলি দেখেন, যদিও, এখন আপনার কোড কীভাবে ঠিক করবেন সে সম্পর্কে আপনার আরও ভাল ধারণা থাকবে!

Note: Pin এবং Unpin-এর এই সংমিশ্রণটি Rust-এ এক সম্পূর্ণ শ্রেণীর জটিল টাইপ নিরাপদে ইমপ্লিমেন্ট করা সম্ভব করে যা অন্যথায় চ্যালেঞ্জিং প্রমাণিত হবে কারণ সেগুলি সেলফ-রেফারেন্সিয়াল। যে টাইপগুলির Pin প্রয়োজন সেগুলি আজ অ্যাসিঙ্ক্রোনাস Rust-এ সবচেয়ে বেশি দেখা যায়, কিন্তু মাঝে মাঝে, আপনি সেগুলিকে অন্যান্য প্রসঙ্গেও দেখতে পারেন।

Pin এবং Unpin কীভাবে কাজ করে এবং তাদের যে নিয়মগুলি বহাল রাখতে হবে তার সুনির্দিষ্ট বিবরণ std::pin-এর জন্য API ডকুমেন্টেশনে ব্যাপকভাবে কভার করা হয়েছে, তাই আপনি যদি আরও জানতে আগ্রহী হন তবে এটি শুরু করার জন্য একটি দুর্দান্ত জায়গা।

আপনি যদি আরও বিশদে হুডের নিচে কীভাবে জিনিসগুলি কাজ করে তা বুঝতে চান তবে Asynchronous Programming in Rust-এর চ্যাপ্টার 2 এবং 4 দেখুন।

Stream Trait

এখন আপনার Future, Pin, এবং Unpin trait গুলির উপর গভীর উপলব্ধি রয়েছে, আমরা Stream trait-এর দিকে আমাদের মনোযোগ দিতে পারি। আপনি যেমন চ্যাপ্টারের শুরুতে শিখেছেন, স্ট্রিমগুলি অ্যাসিঙ্ক্রোনাস ইটারেটরের মতো। Iterator এবং Future-এর বিপরীতে, যদিও, এই লেখার সময় স্ট্যান্ডার্ড লাইব্রেরিতে Stream-এর কোনও সংজ্ঞা নেই, তবে futures ক্রেট থেকে একটি খুব সাধারণ সংজ্ঞা রয়েছে যা ইকোসিস্টেম জুড়ে ব্যবহৃত হয়।

আসুন একটি Stream trait কীভাবে সেগুলিকে একত্রিত করতে পারে তা দেখার আগে Iterator এবং Future trait-এর সংজ্ঞাগুলি পর্যালোচনা করি। Iterator থেকে, আমাদের একটি সিকোয়েন্সের ধারণা রয়েছে: এর next মেথড একটি Option<Self::Item> সরবরাহ করে। Future থেকে, আমাদের সময়ের সাথে প্রস্তুতির ধারণা রয়েছে: এর poll মেথড একটি Poll<Self::Output> সরবরাহ করে। সময়ের সাথে প্রস্তুত হওয়া আইটেমগুলির একটি সিকোয়েন্স উপস্থাপন করতে, আমরা একটি Stream trait সংজ্ঞায়িত করি যা সেই বৈশিষ্ট্যগুলিকে একত্রিত করে:

#![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>>;
}
}

Stream trait স্ট্রিম দ্বারা উৎপাদিত আইটেমগুলির টাইপের জন্য Item নামে একটি অ্যাসোসিয়েটেড টাইপ সংজ্ঞায়িত করে। এটি Iterator-এর অনুরূপ, যেখানে শূন্য থেকে অনেকগুলি আইটেম থাকতে পারে এবং Future-এর বিপরীতে, যেখানে সর্বদা একটি একক Output থাকে, এমনকি যদি এটি ইউনিট টাইপ () হয়।

Stream সেই আইটেমগুলি পাওয়ার জন্য একটি মেথডও সংজ্ঞায়িত করে। আমরা এটিকে poll_next বলি, এটি স্পষ্ট করতে যে এটি Future::poll-এর মতোই পোল করে এবং Iterator::next-এর মতোই আইটেমগুলির একটি সিকোয়েন্স তৈরি করে। এর রিটার্ন টাইপ Poll-কে Option-এর সাথে একত্রিত করে। বাইরের টাইপটি হল Poll, কারণ এটিকে প্রস্তুতির জন্য পরীক্ষা করতে হবে, ঠিক যেমন একটি ফিউচার করে। ভেতরের টাইপটি হল Option, কারণ এটিকে সংকেত দিতে হবে যে আরও মেসেজ আছে কিনা, ঠিক যেমন একটি ইটারেটর করে।

এরকম একটি সংজ্ঞা সম্ভবত Rust-এর স্ট্যান্ডার্ড লাইব্রেরির অংশ হিসাবে শেষ হবে। ইতিমধ্যে, এটি বেশিরভাগ রানটাইমের টুলকিটের অংশ, তাই আপনি এটির উপর নির্ভর করতে পারেন এবং আমরা পরবর্তীতে যা কভার করব তা সাধারণত প্রযোজ্য হওয়া উচিত!

আমরা স্ট্রিমিং-এর বিভাগে যে উদাহরণটি দেখেছি, যদিও, আমরা poll_next বা Stream ব্যবহার করিনি, তবে পরিবর্তে next এবং StreamExt ব্যবহার করেছি। আমরা অবশ্যই poll_next API-এর পরিপ্রেক্ষিতে সরাসরি আমাদের নিজস্ব Stream স্টেট মেশিন হাতে লিখে কাজ করতে পারতাম, ঠিক যেমনটি আমরা তাদের poll মেথডের মাধ্যমে ফিউচারের সাথে সরাসরি কাজ করতে পারতামawait ব্যবহার করা অনেক সুন্দর, যদিও, এবং StreamExt trait 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...
}
}

Note: আমরা চ্যাপ্টারে আগে যে প্রকৃত সংজ্ঞাটি ব্যবহার করেছি তা এর থেকে সামান্য আলাদা দেখায়, কারণ এটি Rust-এর সংস্করণগুলিকে সমর্থন করে যেগুলি এখনও trait-এ অ্যাসিঙ্ক্রোনাস ফাংশন ব্যবহার করা সমর্থন করে না। ফলস্বরূপ, এটি এইরকম দেখায়:

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

সেই Next টাইপটি হল একটি struct যা Future ইমপ্লিমেন্ট করে এবং আমাদের self-এ রেফারেন্সের লাইফটাইমকে Next<'_, Self> দিয়ে নাম দিতে দেয়, যাতে await এই মেথডের সাথে কাজ করতে পারে।

StreamExt trait হল স্ট্রিমগুলির সাথে ব্যবহার করার জন্য উপলব্ধ সমস্ত আকর্ষণীয় মেথডের হোম। StreamExt স্বয়ংক্রিয়ভাবে প্রতিটি টাইপের জন্য ইমপ্লিমেন্ট করা হয় যা Stream ইমপ্লিমেন্ট করে, কিন্তু এই trait গুলিকে আলাদাভাবে সংজ্ঞায়িত করা হয়েছে যাতে কমিউনিটি বেস trait-কে প্রভাবিত না করে সুবিধার API-গুলিতে পুনরাবৃত্তি করতে পারে।

trpl ক্রেটে ব্যবহৃত StreamExt-এর সংস্করণে, trait টি কেবল next মেথড সংজ্ঞায়িত করে না, তবে next-এর একটি ডিফল্ট ইমপ্লিমেন্টেশনও সরবরাহ করে যা Stream::poll_next কল করার বিশদগুলি সঠিকভাবে পরিচালনা করে। এর মানে হল যে এমনকি যখন আপনাকে আপনার নিজের স্ট্রিমিং ডেটা টাইপ লিখতে হবে, তখনও আপনাকে শুধুমাত্র Stream ইমপ্লিমেন্ট করতে হবে এবং তারপর যে কেউ আপনার ডেটা টাইপ ব্যবহার করে সে স্বয়ংক্রিয়ভাবে StreamExt এবং এর মেথডগুলি ব্যবহার করতে পারবে।

এগুলিই আমরা এই trait গুলির নিম্ন-স্তরের বিশদ বিবরণের জন্য কভার করতে যাচ্ছি। শেষ করতে, আসুন বিবেচনা করি কিভাবে ফিউচার (স্ট্রিম সহ), টাস্ক এবং থ্রেডগুলি একসাথে ফিট করে!