অ্যাসিঙ্ক্রোনাসের জন্য ব্যবহৃত 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)
, এবং একটি যার নেই, Pending
। Poll
মানে 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-এ সরলীকৃত চিত্রে দেখানো হয়েছে।
ডিফল্টভাবে, যদিও, যে কোনও অবজেক্ট যার নিজের প্রতি রেফারেন্স রয়েছে সেটি সরানো অনিরাপদ, কারণ রেফারেন্সগুলি সর্বদা তারা যেটিকে রেফার করে তার প্রকৃত মেমরি অ্যাড্রেসের দিকে নির্দেশ করে (চিত্র 17-5 দেখুন)। আপনি যদি ডেটা স্ট্রাকচারটি নিজেই সরিয়ে দেন, তাহলে সেই অভ্যন্তরীণ রেফারেন্সগুলি পুরানো লোকেশনের দিকে নির্দেশ করে থাকবে। যাইহোক, সেই মেমরি লোকেশনটি এখন অবৈধ। একটির জন্য, আপনি যখন ডেটা স্ট্রাকচারে পরিবর্তন করবেন তখন এর মান আপডেট করা হবে না। অন্য—আরও গুরুত্বপূর্ণ—জিনিসের জন্য, কম্পিউটার এখন অন্য উদ্দেশ্যে সেই মেমরিটি পুনরায় ব্যবহার করতে মুক্ত! আপনি পরে সম্পূর্ণ সম্পর্কহীন ডেটা পড়তে পারেন।
তাত্ত্বিকভাবে, Rust কম্পাইলার যখনই কোনও অবজেক্ট সরানো হয় তখনই সেটিতে প্রতিটি রেফারেন্স আপডেট করার চেষ্টা করতে পারে, তবে এটি প্রচুর পারফরম্যান্স ওভারহেড যুক্ত করতে পারে, বিশেষ করে যদি রেফারেন্সের একটি সম্পূর্ণ ওয়েব আপডেট করার প্রয়োজন হয়। পরিবর্তে আমরা যদি নিশ্চিত করতে পারি যে প্রশ্নে থাকা ডেটা স্ট্রাকচারটি মেমরিতে সরানো হচ্ছে না, তাহলে আমাদের কোনও রেফারেন্স আপডেট করতে হবে না। এটিই Rust-এর borrow চেকারের প্রয়োজন: নিরাপদ কোডে, এটি আপনাকে কোনও আইটেমকে সক্রিয় রেফারেন্স সহ সরানোর অনুমতি দেয় না।
Pin
আমাদের প্রয়োজনীয় গ্যারান্টি দেওয়ার জন্য এটির উপর ভিত্তি করে তৈরি। যখন আমরা একটি মানকে পিন করি সেই মানের একটি পয়েন্টারকে Pin
-এ র্যাপ করে, তখন এটি আর সরতে পারে না। সুতরাং, যদি আপনার কাছে Pin<Box<SomeType>>
থাকে, তাহলে আপনি আসলে SomeType
মানটিকে পিন করেন, Box
পয়েন্টারকে নয়। চিত্র 17-6 এই প্রক্রিয়াটি চিত্রিত করে।
প্রকৃতপক্ষে, Box
পয়েন্টারটি এখনও অবাধে ঘোরাফেরা করতে পারে। মনে রাখবেন: আমরা নিশ্চিত করতে চাই যে চূড়ান্তভাবে রেফারেন্স করা ডেটা যথাস্থানে রয়েছে। যদি একটি পয়েন্টার ঘোরাফেরা করে, কিন্তু এটি যে ডেটার দিকে নির্দেশ করে সেটি একই জায়গায় থাকে, যেমনটি চিত্র 17-7-এ রয়েছে, তাহলে কোনও সম্ভাব্য সমস্যা নেই। একটি স্বাধীন অনুশীলন হিসাবে, টাইপগুলির জন্য ডক্স এবং সেইসাথে std::pin
মডিউলটি দেখুন এবং Pin
র্যাপ করা একটি 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-এর বেশিরভাগ অন্যান্য টাইপ করে।
ফলস্বরূপ, আমরা এমন কিছু করতে পারি যা অবৈধ হবে যদি String
পরিবর্তে !Unpin
ইমপ্লিমেন্ট করত, যেমন মেমরিতে ঠিক একই স্থানে একটি স্ট্রিংকে অন্য একটি দিয়ে প্রতিস্থাপন করা যেমনটি চিত্র 17-9-এ রয়েছে। এটি Pin
চুক্তি লঙ্ঘন করে না, কারণ String
-এর কোনও অভ্যন্তরীণ রেফারেন্স নেই যা এটিকে ঘোরাফেরা করা অনিরাপদ করে তোলে! ঠিক এই কারণেই এটি !Unpin
-এর পরিবর্তে Unpin
ইমপ্লিমেন্ট করে।
এখন আমরা 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 গুলির নিম্ন-স্তরের বিশদ বিবরণের জন্য কভার করতে যাচ্ছি। শেষ করতে, আসুন বিবেচনা করি কিভাবে ফিউচার (স্ট্রিম সহ), টাস্ক এবং থ্রেডগুলি একসাথে ফিট করে!