অ্যাসিঙ্ক (Async)-এর জন্য ব্যবহৃত ট্রেইটগুলির একটি নিবিড় পর্যবেক্ষণ
অধ্যায় জুড়ে, আমরা বিভিন্ন উপায়ে Future, Pin, Unpin, Stream, এবং StreamExt ট্রেইটগুলি ব্যবহার করেছি। এখন পর্যন্ত, আমরা এগুলি কীভাবে কাজ করে বা কীভাবে একসাথে খাপ খায় তার বিস্তারিত বিবরণে খুব বেশি যাইনি, যা আপনার দৈনন্দিন রাস্ট কোডিংয়ের জন্য বেশিরভাগ সময় ঠিক আছে। তবে কখনও কখনও, আপনি এমন পরিস্থিতির মুখোমুখি হবেন যেখানে আপনাকে এই বিবরণগুলির আরও কয়েকটি বুঝতে হবে। এই বিভাগে, আমরা সেই পরিস্থিতিগুলিতে সাহায্য করার জন্য যথেষ্ট গভীরে যাব, তবে সত্যিকারের গভীর আলোচনা অন্যান্য ডকুমেন্টেশনের জন্য রেখে দেব।
Future ট্রেইট
আসুন 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>; } }
এই ট্রেইট সংজ্ঞায় একগুচ্ছ নতুন টাইপ এবং এমন কিছু সিনট্যাক্স রয়েছে যা আমরা আগে দেখিনি, তাই আসুন ধাপে ধাপে সংজ্ঞাটি পর্যালোচনা করি।
প্রথমত, Future-এর অ্যাসোসিয়েটেড টাইপ Output বলে যে ফিউচারটি কিসে রিজলভ (resolve) হবে। এটি Iterator ট্রেইটের 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), এবং একটি যার নেই, Pending। তবে Poll-এর অর্থ Option থেকে বেশ ভিন্ন! Pending ভ্যারিয়েন্টটি নির্দেশ করে যে ফিউচারের এখনও কাজ বাকি আছে, তাই কলারকে পরে আবার পরীক্ষা করতে হবে। Ready ভ্যারিয়েন্টটি নির্দেশ করে যে ফিউচারটি তার কাজ শেষ করেছে এবং T মানটি উপলব্ধ।
দ্রষ্টব্য: বেশিরভাগ ফিউচারের ক্ষেত্রে, ফিউচারটি
Readyরিটার্ন করার পরে কলারের আবারpollকল করা উচিত নয়। অনেক ফিউচার রেডি (ready) হওয়ার পরে আবার পোল করা হলে প্যানিক (panic) করবে। যে ফিউচারগুলি আবার পোল করা নিরাপদ, সেগুলি তাদের ডকুমেন্টেশনে স্পষ্টভাবে উল্লেখ করবে। এটিIterator::nextযেভাবে আচরণ করে তার অনুরূপ।
যখন আপনি await ব্যবহার করে কোড দেখেন, তখন রাস্ট পর্দার আড়ালে এটিকে poll কল করে এমন কোডে কম্পাইল করে। আপনি যদি লিস্টিং ১৭-৪-এ ফিরে তাকান, যেখানে আমরা একটি একক URL-এর পেজ টাইটেল রিজলভ হওয়ার পরে প্রিন্ট করেছিলাম, রাস্ট এটিকে প্রায় (যদিও ঠিক নয়) এইরকম কিছুতে কম্পাইল করে:
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 => {
// কিন্তু এখানে কী হবে?
}
}
ফিউচারটি যখন এখনও 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
}
}
}
তবে যদি রাস্ট এটিকে ঠিক সেই কোডে কম্পাইল করত, তবে প্রতিটি await ব্লকিং হয়ে যেত—আমরা যা চেয়েছিলাম তার ঠিক বিপরীত! পরিবর্তে, রাস্ট নিশ্চিত করে যে লুপটি এমন কিছুর কাছে নিয়ন্ত্রণ হস্তান্তর করতে পারে যা এই ফিউচারের কাজ পজ (pause) করে অন্য ফিউচারগুলিতে কাজ করতে পারে এবং তারপরে এটিকে আবার পরীক্ষা করতে পারে। যেমন আমরা দেখেছি, সেই কিছু হলো একটি async runtime, এবং এই সময়সূচী এবং সমন্বয়ের কাজটি এর অন্যতম প্রধান কাজ।
অধ্যায়ের শুরুতে, আমরা rx.recv-এর জন্য অপেক্ষা করার বর্ণনা দিয়েছিলাম। recv কল একটি ফিউচার রিটার্ন করে, এবং ফিউচারটি await করা এটিকে পোল করে। আমরা উল্লেখ করেছি যে একটি রানটাইম ফিউচারটিকে পজ করবে যতক্ষণ না এটি Some(message) বা চ্যানেল বন্ধ হয়ে গেলে None-এর সাথে রেডি হয়। Future ট্রেইট এবং বিশেষ করে Future::poll সম্পর্কে আমাদের গভীর বোঝার সাথে, আমরা দেখতে পাচ্ছি এটি কীভাবে কাজ করে। রানটাইম জানে যে ফিউচারটি রেডি নয় যখন এটি Poll::Pending রিটার্ন করে। বিপরীতভাবে, রানটাইম জানে যে ফিউচারটি রেডি এবং poll যখন Poll::Ready(Some(message)) বা Poll::Ready(None) রিটার্ন করে তখন এটিকে এগিয়ে নিয়ে যায়।
একটি রানটাইম কীভাবে এটি করে তার সঠিক বিবরণ এই বইয়ের সুযোগের বাইরে, কিন্তু মূল বিষয় হলো ফিউচারের মৌলিক মেকানিক্স দেখা: একটি রানটাইম তার দায়িত্বে থাকা প্রতিটি ফিউচারকে পোল করে, যখন এটি এখনও রেডি না হয় তখন ফিউচারটিকে ঘুমাতে পাঠায়।
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}`
|
= 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`
এই ত্রুটি বার্তাটি আমাদের কেবল এটিই বলে না যে আমাদের মানগুলি পিন করতে হবে, বরং পিনিং কেন প্রয়োজন তাও বলে। trpl::join_all ফাংশনটি JoinAll নামে একটি struct রিটার্ন করে। সেই struct-টি F টাইপের উপর জেনেরিক, যা Future ট্রেইট ইমপ্লিমেন্ট করার জন্য সীমাবদ্ধ। await দিয়ে সরাসরি একটি ফিউচার await করা ফিউচারটিকে অন্তর্নিহিতভাবে পিন করে। একারণে আমাদের যেখানেই ফিউচার await করতে চাই সেখানে pin! ব্যবহার করার প্রয়োজন হয় না।
যাইহোক, আমরা এখানে সরাসরি একটি ফিউচার await করছি না। পরিবর্তে, আমরা join_all ফাংশনে ফিউচারের একটি কালেকশন পাস করে একটি নতুন ফিউচার, JoinAll তৈরি করছি। join_all-এর সিগনেচার প্রয়োজন করে যে কালেকশনের আইটেমগুলির টাইপগুলি সবই Future ট্রেইট ইমপ্লিমেন্ট করে, এবং Box<T> কেবল তখনই Future ইমপ্লিমেন্ট করে যদি এটি যে T-কে র্যাপ (wrap) করে তা Unpin ট্রেইট ইমপ্লিমেন্ট করা একটি ফিউচার হয়।
এটি হজম করার জন্য অনেক কিছু! এটি সত্যিই বুঝতে হলে, আসুন Future ট্রেইটটি আসলে কীভাবে কাজ করে, বিশেষ করে পিনিং এর আশেপাশে, সে সম্পর্কে আরও গভীরে যাই।
Future ট্রেইটের সংজ্ঞায় আবার দেখুন:
#![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-এর জন্য একটি টাইপ অ্যানোটেশন অন্যান্য ফাংশন প্যারামিটারের জন্য টাইপ অ্যানোটেশনের মতোই কাজ করে, তবে দুটি মূল পার্থক্য রয়েছে:
- এটি রাস্টকে বলে যে মেথডটি কল করার জন্য
selfকোন টাইপের হতে হবে। - এটি যেকোনো টাইপ হতে পারে না। এটি যে টাইপের উপর মেথডটি ইমপ্লিমেন্ট করা হয়েছে, সেই টাইপের একটি রেফারেন্স বা স্মার্ট পয়েন্টার, বা সেই টাইপের একটি রেফারেন্সকে র্যাপ করা একটি
Pin-এর মধ্যে সীমাবদ্ধ।
আমরা চ্যাপ্টার ১৮-এ এই সিনট্যাক্স সম্পর্কে আরও দেখব। আপাতত, এটি জানাই যথেষ্ট যে যদি আমরা একটি ফিউচার পোল করতে চাই এটি Pending নাকি Ready(Output) তা পরীক্ষা করার জন্য, আমাদের টাইপের একটি Pin-র্যাপ করা মিউটেবল রেফারেন্স প্রয়োজন।
Pin হলো &, &mut, Box, এবং Rc-এর মতো পয়েন্টার-সদৃশ টাইপের জন্য একটি র্যাপার। (টেকনিক্যালি, Pin Deref বা DerefMut ট্রেইট ইমপ্লিমেন্ট করা টাইপের সাথে কাজ করে, তবে এটি কার্যকরভাবে কেবল পয়েন্টারের সাথে কাজ করার সমতুল্য।) Pin নিজে কোনো পয়েন্টার নয় এবং এর নিজস্ব কোনো আচরণ নেই যেমন Rc এবং Arc-এর রেফারেন্স কাউন্টিংয়ের সাথে আছে; এটি সম্পূর্ণরূপে একটি টুল যা কম্পাইলার পয়েন্টার ব্যবহারের উপর সীমাবদ্ধতা আরোপ করতে ব্যবহার করতে পারে।
await-কে poll কলের মাধ্যমে ইমপ্লিমেন্ট করা হয়েছে মনে করলে, আমরা আগে যে ত্রুটি বার্তাটি দেখেছিলাম তা ব্যাখ্যা করা শুরু হয়, কিন্তু সেটি ছিল Unpin-এর ক্ষেত্রে, Pin-এর নয়। তাহলে Pin কীভাবে Unpin-এর সাথে সম্পর্কিত, এবং poll কল করার জন্য Future-এর self-কে একটি Pin টাইপের মধ্যে থাকতে হবে কেন?
এই অধ্যায়ের শুরুতে মনে করুন, একটি ফিউচারের মধ্যে একাধিক await পয়েন্ট একটি স্টেট মেশিনে কম্পাইল হয়, এবং কম্পাইলার নিশ্চিত করে যে সেই স্টেট মেশিনটি রাস্টের স্বাভাবিক নিরাপত্তা নিয়মগুলি, যার মধ্যে borrowing এবং ownership রয়েছে, অনুসরণ করে। এটি কাজ করানোর জন্য, রাস্ট দেখে যে একটি await পয়েন্ট এবং পরবর্তী await পয়েন্ট বা async ব্লকের শেষের মধ্যে কোন ডেটা প্রয়োজন। তারপরে এটি কম্পাইল করা স্টেট মেশিনে একটি সংশ্লিষ্ট ভ্যারিয়েন্ট তৈরি করে। প্রতিটি ভ্যারিয়েন্ট সোর্স কোডের সেই বিভাগে ব্যবহৃত ডেটাতে প্রয়োজনীয় অ্যাক্সেস পায়, হয় সেই ডেটার মালিকানা নিয়ে অথবা এর একটি মিউটেবল বা অপরিবর্তনীয় রেফারেন্স পেয়ে।
এখন পর্যন্ত, সব ঠিক আছে: যদি আমরা একটি নির্দিষ্ট async ব্লকে মালিকানা বা রেফারেন্স সম্পর্কে কোনো ভুল করি, borrow checker আমাদের বলে দেবে। যখন আমরা সেই ব্লকের সাথে সঙ্গতিপূর্ণ ফিউচারটি সরাতে চাই—যেমন join_all-এ পাস করার জন্য এটিকে একটি Vec-এ সরানো—তখন জিনিসগুলি আরও জটিল হয়ে যায়।
যখন আমরা একটি ফিউচার সরাই—সেটি join_all-এর সাথে ইটারেটর হিসাবে ব্যবহার করার জন্য একটি ডেটা স্ট্রাকচারে পুশ করে হোক বা একটি ফাংশন থেকে এটি রিটার্ন করে হোক—এর অর্থ আসলে রাস্ট আমাদের জন্য যে স্টেট মেশিন তৈরি করে তা সরানো। এবং রাস্টের বেশিরভাগ অন্যান্য টাইপের থেকে ভিন্ন, async ব্লকের জন্য রাস্ট যে ফিউচারগুলি তৈরি করে সেগুলি যেকোনো প্রদত্ত ভ্যারিয়েন্টের ফিল্ডে নিজেদের রেফারেন্স দিয়ে শেষ হতে পারে, যেমনটি চিত্র ১৭-৪-এর সরলীকৃত চিত্রে দেখানো হয়েছে।
ডিফল্টরূপে, যে কোনো অবজেক্ট যার নিজের কাছে একটি রেফারেন্স আছে তা সরানো অনিরাপদ, কারণ রেফারেন্সগুলি সর্বদা তাদের উল্লেখ করা জিনিসের আসল মেমরি ঠিকানায় নির্দেশ করে (চিত্র ১৭-৫ দেখুন)। আপনি যদি ডেটা স্ট্রাকচারটি নিজেই সরান, তবে সেই অভ্যন্তরীণ রেফারেন্সগুলি পুরানো অবস্থানে নির্দেশ করতে থাকবে। যাইহোক, সেই মেমরি অবস্থানটি এখন অবৈধ। একটি কারণ হলো, আপনি ডেটা স্ট্রাকচারে পরিবর্তন করলে এর মান আপডেট হবে না। আরেকটি—আরও গুরুত্বপূর্ণ—কারণ হলো, কম্পিউটার এখন সেই মেমরিটি অন্যান্য উদ্দেশ্যে পুনরায় ব্যবহার করতে স্বাধীন! আপনি পরে সম্পূর্ণ সম্পর্কহীন ডেটা পড়তে পারেন।
তাত্ত্বিকভাবে, রাস্ট কম্পাইলার যখনই কোনো অবজেক্ট সরানো হয় তখন সেটির প্রতিটি রেফারেন্স আপডেট করার চেষ্টা করতে পারত, কিন্তু এটি অনেক পারফরম্যান্স ওভারহেড যোগ করতে পারত, বিশেষ করে যদি রেফারেন্সের পুরো একটি জাল আপডেট করার প্রয়োজন হয়। যদি আমরা পরিবর্তে নিশ্চিত করতে পারতাম যে প্রশ্নবিদ্ধ ডেটা স্ট্রাকচারটি মেমরিতে নড়াচড়া করে না, তাহলে আমাদের কোনো রেফারেন্স আপডেট করতে হতো না। রাস্টের borrow checker ঠিক এটাই প্রয়োজন করে: নিরাপদ কোডে, এটি আপনাকে এমন কোনো আইটেম সরাতে বাধা দেয় যার কাছে একটি সক্রিয় রেফারেন্স রয়েছে।
Pin এর উপর ভিত্তি করে আমাদের ঠিক সেই গ্যারান্টি দেয় যা আমাদের প্রয়োজন। যখন আমরা একটি মানকে পিন করি সেই মানের একটি পয়েন্টারকে Pin-এ র্যাপ করে, তখন এটি আর নড়াচড়া করতে পারে না। সুতরাং, যদি আপনার Pin<Box<SomeType>> থাকে, আপনি আসলে SomeType মানটি পিন করেন, Box পয়েন্টারটি নয়। চিত্র ১৭-৬ এই প্রক্রিয়াটি চিত্রিত করে।
<img alt="পাশাপাশি রাখা তিনটি বক্স। প্রথমটির লেবেল "Pin", দ্বিতীয়টির "b1", এবং তৃতীয়টির "pinned"। "pinned"-এর মধ্যে একটি টেবিল রয়েছে যার লেবেল "fut", একটি একক কলাম সহ; এটি ডেটা স্ট্রাকচারের প্রতিটি অংশের জন্য সেল সহ একটি ফিউচার প্রতিনিধিত্ব করে। এর প্রথম সেলে "0" মান রয়েছে, এর দ্বিতীয় সেল থেকে একটি তীর বেরিয়ে চতুর্থ এবং শেষ সেলে নির্দেশ করছে, যার মধ্যে "1" মান রয়েছে, এবং তৃতীয় সেলে ড্যাশড লাইন এবং একটি এলিপসিস রয়েছে যা নির্দেশ করে যে ডেটা স্ট্রাকচারের অন্যান্য অংশ থাকতে পারে। সব মিলিয়ে, "fut" টেবিলটি একটি ফিউচার প্রতিনিধিত্ব করে যা সেলফ-রেফারেনশিয়াল। "Pin" লেবেলযুক্ত বক্স থেকে একটি তীর বেরিয়ে, "b1" বক্সের মধ্য দিয়ে গিয়ে "pinned" বক্সের ভিতরে "fut" টেবিলে শেষ হয়।" src="img/trpl17-06.svg" class="center" />
আসলে, Box পয়েন্টারটি এখনও অবাধে নড়াচড়া করতে পারে। মনে রাখবেন: আমরা নিশ্চিত করতে চাই যে অবশেষে রেফারেন্স করা ডেটা জায়গায় থাকে। যদি একটি পয়েন্টার নড়াচড়া করে, কিন্তু এটি যে ডেটার দিকে নির্দেশ করে তা একই জায়গায় থাকে, যেমন চিত্র ১৭-৭-এ, কোনো সম্ভাব্য সমস্যা নেই। একটি স্বতন্ত্র অনুশীলন হিসাবে, টাইপগুলির ডকুমেন্টেশন এবং std::pin মডিউল দেখুন এবং একটি Pin র্যাপিং Box-এর সাথে এটি কীভাবে করবেন তা বের করার চেষ্টা করুন।) মূল বিষয় হলো সেলফ-রেফারেনশিয়াল টাইপটি নিজে নড়াচড়া করতে পারে না, কারণ এটি এখনও পিন করা আছে।
<img alt="তিনটি মোটামুটি কলামে রাখা চারটি বক্স, যা পূর্ববর্তী ডায়াগ্রামের মতোই তবে দ্বিতীয় কলামে একটি পরিবর্তন সহ। এখন দ্বিতীয় কলামে দুটি বক্স আছে, "b1" এবং "b2" লেবেলযুক্ত, "b1" ধূসর রঙের, এবং "Pin" থেকে তীরটি "b1"-এর পরিবর্তে "b2"-এর মধ্য দিয়ে যায়, যা নির্দেশ করে যে পয়েন্টারটি "b1" থেকে "b2"-তে সরে গেছে, কিন্তু "pinned"-এর ডেটা সরেনি।" src="img/trpl17-07.svg" class="center" />
যাইহোক, বেশিরভাগ টাইপই চারপাশে সরানো পুরোপুরি নিরাপদ, এমনকি যদি সেগুলি একটি Pin র্যাপারের পিছনে থাকে। আমাদের কেবল তখনই পিনিং সম্পর্কে ভাবতে হবে যখন আইটেমগুলির অভ্যন্তরীণ রেফারেন্স থাকে। সংখ্যা এবং বুলিয়ানের মতো আদিম মানগুলি নিরাপদ কারণ তাদের স্পষ্টতই কোনো অভ্যন্তরীণ রেফারেন্স নেই। রাস্টে আপনি সাধারণত যে বেশিরভাগ টাইপের সাথে কাজ করেন সেগুলিও নেই। আপনি উদাহরণস্বরূপ, একটি Vec চারপাশে সরাতে পারেন, কোনো চিন্তা ছাড়াই। আমরা এখন পর্যন্ত যা দেখেছি তা দিয়ে, যদি আপনার একটি Pin<Vec<String>> থাকে, তবে আপনাকে Pin দ্বারা প্রদত্ত নিরাপদ কিন্তু সীমাবদ্ধ API-এর মাধ্যমে সবকিছু করতে হতো, যদিও একটি Vec<String> সর্বদা সরানো নিরাপদ যদি এর অন্য কোনো রেফারেন্স না থাকে। আমাদের কম্পাইলারকে বলার একটি উপায় প্রয়োজন যে এই ধরনের ক্ষেত্রে আইটেমগুলি চারপাশে সরানো ঠিক আছে—এবং এখানেই Unpin কাজে আসে।
Unpin একটি মার্কার ট্রেইট, যা আমরা চ্যাপ্টার ১৬-তে দেখা Send এবং Sync ট্রেইটের মতো, এবং তাই এর নিজস্ব কোনো কার্যকারিতা নেই। মার্কার ট্রেইটগুলি কেবল কম্পাইলারকে বলার জন্য বিদ্যমান যে একটি নির্দিষ্ট প্রসঙ্গে একটি প্রদত্ত ট্রেইট ইমপ্লিমেন্ট করা টাইপ ব্যবহার করা নিরাপদ। 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-এ র্যাপ করতে পারি, যেমন চিত্র ১৭-৮-এ দেখা গেছে। যাইহোক, String স্বয়ংক্রিয়ভাবে Unpin ইমপ্লিমেন্ট করে, যেমন রাস্টের বেশিরভাগ অন্যান্য টাইপ করে।
ফলস্বরূপ, আমরা এমন কিছু করতে পারি যা অবৈধ হতো যদি String !Unpin ইমপ্লিমেন্ট করত, যেমন মেমরিতে ঠিক একই স্থানে একটি স্ট্রিংকে অন্য একটি দিয়ে প্রতিস্থাপন করা, যেমন চিত্র ১৭-৯-এ। এটি Pin চুক্তি লঙ্ঘন করে না, কারণ String-এর কোনো অভ্যন্তরীণ রেফারেন্স নেই যা এটিকে চারপাশে সরানো অনিরাপদ করে! ঠিক একারণেই এটি !Unpin-এর পরিবর্তে Unpin ইমপ্লিমেন্ট করে।
এখন আমরা লিস্টিং ১৭-১৭ থেকে সেই join_all কলের জন্য রিপোর্ট করা ত্রুটিগুলি বোঝার জন্য যথেষ্ট জানি। আমরা মূলত async ব্লক দ্বারা উৎপাদিত ফিউচারগুলিকে একটি Vec<Box<dyn Future<Output = ()>>>-এ সরানোর চেষ্টা করেছিলাম, কিন্তু যেমন আমরা দেখেছি, সেই ফিউচারগুলির অভ্যন্তরীণ রেফারেন্স থাকতে পারে, তাই তারা Unpin ইমপ্লিমেন্ট করে না। তাদের পিন করা দরকার, এবং তারপরে আমরা Pin টাইপটিকে Vec-এ পাস করতে পারি, আত্মবিশ্বাসী যে ফিউচারের অন্তর্নিহিত ডেটা সরানো হবে না।
Pin এবং Unpin বেশিরভাগই নিম্ন-স্তরের লাইব্রেরি তৈরির জন্য, অথবা যখন আপনি নিজে একটি রানটাইম তৈরি করছেন, দৈনন্দিন রাস্ট কোডের জন্য ততটা গুরুত্বপূর্ণ নয়। তবে যখন আপনি ত্রুটি বার্তাগুলিতে এই ট্রেইটগুলি দেখেন, এখন আপনার কোড কীভাবে ঠিক করতে হবে সে সম্পর্কে আরও ভালো ধারণা থাকবে!
দ্রষ্টব্য:
PinএবংUnpin-এর এই সংমিশ্রণটি রাস্টে এক শ্রেণীর জটিল টাইপ নিরাপদে ইমপ্লিমেন্ট করা সম্ভব করে তোলে যা অন্যথায় চ্যালেঞ্জিং প্রমাণিত হতো কারণ সেগুলি সেলফ-রেফারেনশিয়াল। যে টাইপগুলিরPinপ্রয়োজন সেগুলি আজ async রাস্টে সবচেয়ে বেশি দেখা যায়, তবে মাঝে মাঝে, আপনি সেগুলিকে অন্যান্য প্রসঙ্গেও দেখতে পারেন।
PinএবংUnpinকীভাবে কাজ করে, এবং তাদের যে নিয়মগুলি বজায় রাখতে হয়, সেগুলিstd::pin-এর জন্য API ডকুমেন্টেশনে ব্যাপকভাবে আচ্ছাদিত, তাই আপনি যদি আরও শিখতে আগ্রহী হন, তবে এটি শুরু করার জন্য একটি দুর্দান্ত জায়গা।আপনি যদি পর্দার আড়ালে জিনিসগুলি কীভাবে কাজ করে তা আরও বিস্তারিতভাবে বুঝতে চান, Asynchronous Programming in Rust-এর ২ এবং ৪ অধ্যায় দেখুন।
Stream ট্রেইট
এখন যেহেতু আপনার Future, Pin, এবং Unpin ট্রেইটগুলির উপর গভীর ধারণা আছে, আমরা Stream ট্রেইটের দিকে আমাদের মনোযোগ ফেরাতে পারি। যেমন আপনি অধ্যায়ের শুরুতে শিখেছেন, স্ট্রীমগুলি অ্যাসিঙ্ক্রোনাস ইটারেটরের মতো। Iterator এবং Future-এর থেকে ভিন্ন, Stream-এর এই লেখার সময় স্ট্যান্ডার্ড লাইব্রেরিতে কোনো সংজ্ঞা নেই, তবে futures ক্রেট থেকে একটি খুব সাধারণ সংজ্ঞা রয়েছে যা ইকোসিস্টেম জুড়ে ব্যবহৃত হয়।
আসুন Iterator এবং Future ট্রেইটগুলির সংজ্ঞা পর্যালোচনা করি একটি Stream ট্রেইট কীভাবে সেগুলিকে একত্রিত করতে পারে তা দেখার আগে। Iterator থেকে, আমাদের কাছে একটি ক্রমের ধারণা রয়েছে: এর next মেথড একটি Option<Self::Item> সরবরাহ করে। Future থেকে, আমাদের কাছে সময়ের সাথে সাথে প্রস্তুতির ধারণা রয়েছে: এর poll মেথড একটি Poll<Self::Output> সরবরাহ করে। সময়ের সাথে সাথে প্রস্তুত হওয়া আইটেমগুলির একটি ক্রম উপস্থাপন করতে, আমরা একটি 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>>; } }
Stream ট্রেইটটি স্ট্রীম দ্বারা উৎপাদিত আইটেমগুলির টাইপের জন্য Item নামে একটি অ্যাসোসিয়েটেড টাইপ সংজ্ঞায়িত করে। এটি Iterator-এর মতো, যেখানে শূন্য থেকে অনেক আইটেম থাকতে পারে, এবং Future-এর থেকে ভিন্ন, যেখানে সর্বদা একটি একক Output থাকে, এমনকি যদি এটি ইউনিট টাইপ () হয়।
Stream-এর সেই আইটেমগুলি পাওয়ার জন্য একটি মেথডও সংজ্ঞায়িত করে। আমরা এটিকে poll_next বলি, এটি স্পষ্ট করার জন্য যে এটি Future::poll-এর মতোই পোল করে এবং Iterator::next-এর মতোই আইটেমের একটি ক্রম তৈরি করে। এর রিটার্ন টাইপ Poll-কে Option-এর সাথে একত্রিত করে। বাইরের টাইপটি Poll, কারণ এটি প্রস্তুতির জন্য পরীক্ষা করতে হবে, ঠিক একটি ফিউচারের মতো। ভেতরের টাইপটি Option, কারণ এটি আরও বার্তা আছে কিনা তা সংকেত দিতে হবে, ঠিক একটি ইটারেটরের মতো।
এরকম কিছু সংজ্ঞা সম্ভবত রাস্টের স্ট্যান্ডার্ড লাইব্রেরির অংশ হিসাবে শেষ হবে। এর মধ্যে, এটি বেশিরভাগ রানটাইমের টুলকিটের অংশ, তাই আপনি এটির উপর নির্ভর করতে পারেন, এবং আমরা পরবর্তীতে যা কিছু কভার করব তা সাধারণত প্রযোজ্য হবে!
স্ট্রিমিং সম্পর্কিত বিভাগে আমরা যে উদাহরণটি দেখেছি, সেখানে আমরা poll_next বা Stream ব্যবহার করিনি, বরং next এবং StreamExt ব্যবহার করেছি। আমরা অবশ্যই poll_next API-এর ভিত্তিতে সরাসরি কাজ করতে পারতাম, আমাদের নিজস্ব Stream স্টেট মেশিন হাতে লিখে, ঠিক যেমন আমরা ফিউচারের সাথে সরাসরি তাদের poll মেথডের মাধ্যমে কাজ করতে পারতাম। তবে await ব্যবহার করা অনেক সুন্দর, এবং 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 ফাংশন ব্যবহার করার সমর্থন ছিল না। ফলস্বরূপ, এটি এইরকম দেখায়:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;সেই
Nextটাইপটি হলো একটিstructযাFutureইমপ্লিমেন্ট করে এবং আমাদেরself-এর রেফারেন্সের লাইফটাইমকেNext<'_, Self>দিয়ে নামকরণ করার অনুমতি দেয়, যাতেawaitএই মেথডের সাথে কাজ করতে পারে।
StreamExt ট্রেইটটি স্ট্রীমের সাথে ব্যবহার করার জন্য উপলব্ধ সমস্ত আকর্ষণীয় মেথডেরও হোম। StreamExt স্বয়ংক্রিয়ভাবে প্রতিটি টাইপের জন্য ইমপ্লিমেন্ট করা হয় যা Stream ইমপ্লিমেন্ট করে, তবে এই ট্রেইটগুলি আলাদাভাবে সংজ্ঞায়িত করা হয়েছে যাতে কমিউনিটি ফাউন্ডেশনাল ট্রেইটকে প্রভাবিত না করে সুবিধাজনক API-গুলির উপর পুনরাবৃত্তি করতে পারে।
trpl ক্রেটে ব্যবহৃত StreamExt-এর সংস্করণে, ট্রেইটটি কেবল next মেথড সংজ্ঞায়িত করে না, বরং next-এর একটি ডিফল্ট ইমপ্লিমেন্টেশনও সরবরাহ করে যা Stream::poll_next কল করার বিবরণগুলি সঠিকভাবে পরিচালনা করে। এর মানে হলো এমনকি যখন আপনার নিজের স্ট্রিমিং ডেটা টাইপ লিখতে হবে, তখন আপনাকে কেবলমাত্র Stream ইমপ্লিমেন্ট করতে হবে, এবং তারপরে যে কেউ আপনার ডেটা টাইপ ব্যবহার করে সে স্বয়ংক্রিয়ভাবে StreamExt এবং এর মেথডগুলি ব্যবহার করতে পারবে।
এই ট্রেইটগুলির নিম্ন-স্তরের বিবরণ সম্পর্কে আমরা কেবল এটুকুই কভার করব। শেষ করার জন্য, আসুন বিবেচনা করি কীভাবে ফিউচার (স্ট্রীম সহ), টাস্ক এবং থ্রেড সব একসাথে খাপ খায়!