Futures এবং Async সিনট্যাক্স
রাস্টে অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের মূল উপাদানগুলো হলো futures এবং রাস্টের async
ও await
কিওয়ার্ড।
একটি future হলো এমন একটি মান যা এখন প্রস্তুত নাও থাকতে পারে তবে ভবিষ্যতে কোনো এক সময়ে প্রস্তুত হবে। (এই একই ধারণাটি অনেক ভাষায় দেখা যায়, কখনও কখনও task বা promise এর মতো অন্য নামেও পরিচিত।) রাস্ট একটি Future
trait প্রদান করে যা একটি বিল্ডিং ব্লক হিসেবে কাজ করে, যাতে বিভিন্ন ডেটা স্ট্রাকচার দিয়ে বিভিন্ন অ্যাসিঙ্ক্রোনাস অপারেশন প্রয়োগ করা যায় কিন্তু একটি সাধারণ ইন্টারফেসের মাধ্যমে। রাস্টে, futures হলো সেইসব টাইপ যা Future
trait প্রয়োগ করে। প্রতিটি future তার নিজের অগ্রগতি এবং "প্রস্তুত" হওয়ার অর্থ কী সে সম্পর্কে তথ্য ধরে রাখে।
ব্লক এবং ফাংশনগুলোতে async
কিওয়ার্ড প্রয়োগ করে আপনি নির্দিষ্ট করতে পারেন যে সেগুলোকে বাধা দেওয়া এবং পুনরায় চালু করা যেতে পারে। একটি async ব্লক বা async ফাংশনের মধ্যে, আপনি একটি future-এর জন্য অপেক্ষা করতে (অর্থাৎ, এটি প্রস্তুত হওয়ার জন্য অপেক্ষা করতে) await
কিওয়ার্ডটি ব্যবহার করতে পারেন। একটি async ব্লক বা ফাংশনের মধ্যে যেখানেই আপনি একটি future-এর জন্য await করেন, সেটি সেই async ব্লক বা ফাংশনের জন্য থামা এবং পুনরায় চালু হওয়ার একটি সম্ভাব্য স্থান। একটি future-এর মান উপলব্ধ হয়েছে কিনা তা পরীক্ষা করার প্রক্রিয়াকে পোলিং (polling) বলা হয়।
কিছু অন্য ভাষা, যেমন C# এবং JavaScript, অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের জন্য async
এবং await
কিওয়ার্ড ব্যবহার করে। আপনি যদি সেই ভাষাগুলির সাথে পরিচিত হন, তবে রাস্ট কীভাবে কাজ করে, সিনট্যাক্স কীভাবে পরিচালনা করে সহ কিছু গুরুত্বপূর্ণ পার্থক্য লক্ষ্য করতে পারেন। এর পেছনে সঙ্গত কারণ রয়েছে, যা আমরা দেখব!
async রাস্ট লেখার সময়, আমরা বেশিরভাগ সময় async
এবং await
কিওয়ার্ড ব্যবহার করি। রাস্ট সেগুলোকে Future
trait ব্যবহার করে সমতুল্য কোডে কম্পাইল করে, যেমনটি এটি for
লুপকে Iterator
trait ব্যবহার করে সমতুল্য কোডে কম্পাইল করে। তবে, যেহেতু রাস্ট Future
trait প্রদান করে, তাই প্রয়োজনে আপনি আপনার নিজের ডেটা টাইপের জন্য এটি প্রয়োগ করতে পারেন। এই অধ্যায়ে আমরা যে ফাংশনগুলো দেখব তার মধ্যে অনেকগুলিই তাদের নিজস্ব Future
এর ইমপ্লিমেন্টেশন সহ টাইপ রিটার্ন করে। আমরা অধ্যায়ের শেষে trait-টির সংজ্ঞায় ফিরে আসব এবং এটি কীভাবে কাজ করে সে সম্পর্কে আরও গভীরে যাব, কিন্তু আপাতত এগিয়ে যাওয়ার জন্য এইটুকুই যথেষ্ট।
এই সবকিছু কিছুটা বিমূর্ত মনে হতে পারে, তাই চলুন আমাদের প্রথম async প্রোগ্রামটি লিখি: একটি ছোট ওয়েব স্ক্র্যাপার। আমরা কমান্ড লাইন থেকে দুটি ইউআরএল (URL) নেব, উভয়ই কনকারেন্টলি ফেচ করব, এবং যেটি প্রথমে শেষ হবে তার ফলাফল ফেরত দেব। এই উদাহরণে বেশ কিছু নতুন সিনট্যাক্স থাকবে, কিন্তু চিন্তা করবেন না—আমরা যেতে যেতে আপনার যা যা জানা দরকার তার সবকিছু ব্যাখ্যা করব।
আমাদের প্রথম অ্যাসিঙ্ক্রোনাস প্রোগ্রাম
এই অধ্যায়ের ফোকাস async শেখার উপর রাখতে এবং ইকোসিস্টেমের বিভিন্ন অংশ নিয়ে মাথা না ঘামানোর জন্য, আমরা trpl
crate তৈরি করেছি (trpl
হলো “The Rust Programming Language” এর সংক্ষিপ্ত রূপ)। এটি আপনার প্রয়োজনীয় সমস্ত টাইপ, ট্রেইট এবং ফাংশন পুনরায় এক্সপোর্ট করে, প্রধানত futures
এবং tokio
crate থেকে। futures
crate হলো async কোডের জন্য রাস্টের পরীক্ষামূলক কাজের একটি অফিসিয়াল স্থান, এবং এখানেই মূলত Future
trait ডিজাইন করা হয়েছিল। Tokio বর্তমানে রাস্টের সবচেয়ে বহুল ব্যবহৃত async runtime, বিশেষ করে ওয়েব অ্যাপ্লিকেশনের জন্য। আরও অনেক ভালো রানটাইম রয়েছে এবং সেগুলো আপনার প্রয়োজনের জন্য আরও উপযুক্ত হতে পারে। আমরা trpl
-এর জন্য পর্দার আড়ালে tokio
crate ব্যবহার করি কারণ এটি ভালোভাবে পরীক্ষিত এবং ব্যাপকভাবে ব্যবহৃত।
কিছু ক্ষেত্রে, trpl
মূল API-গুলিকে পুনঃনামকরণ বা র্যাপ (wrap) করে যাতে আপনি এই অধ্যায়ের প্রাসঙ্গিক বিবরণগুলিতে মনোনিবেশ করতে পারেন। যদি আপনি বুঝতে চান crate-টি কী করে, আমরা আপনাকে এর সোর্স কোড দেখতে উৎসাহিত করি। আপনি দেখতে পারবেন প্রতিটি রি-এক্সপোর্ট কোন crate থেকে আসে, এবং আমরা crate-টি কী করে তা ব্যাখ্যা করার জন্য বিশদ মন্তব্য রেখেছি।
hello-async
নামে একটি নতুন বাইনারি প্রজেক্ট তৈরি করুন এবং trpl
crate-কে একটি ডিপেন্ডেন্সি হিসেবে যোগ করুন:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
এখন আমরা আমাদের প্রথম async প্রোগ্রাম লেখার জন্য trpl
দ্বারা প্রদত্ত বিভিন্ন অংশ ব্যবহার করতে পারি। আমরা একটি ছোট কমান্ড লাইন টুল তৈরি করব যা দুটি ওয়েব পেজ ফেচ করে, প্রতিটির <title>
এলিমেন্ট বের করে এবং যে পেজটি এই পুরো প্রক্রিয়াটি প্রথমে শেষ করবে তার টাইটেল প্রিন্ট করবে।
page_title ফাংশনটি সংজ্ঞায়িত করা
আসুন একটি ফাংশন লেখার মাধ্যমে শুরু করি যা একটি পেজের URL প্যারামিটার হিসেবে নেয়, সেটিতে একটি রিকোয়েস্ট করে, এবং টাইটেল এলিমেন্টের টেক্সট রিটার্ন করে (দেখুন লিস্টিং ১৭-১)।
extern crate trpl; // required for mdbook test fn main() { // TODO: we'll add this next! } use trpl::Html; async fn page_title(url: &str) -> Option<String> { let response = trpl::get(url).await; let response_text = response.text().await; Html::parse(&response_text) .select_first("title") .map(|title| title.inner_html()) }
প্রথমে, আমরা page_title
নামে একটি ফাংশন সংজ্ঞায়িত করি এবং এটিকে async
কিওয়ার্ড দিয়ে চিহ্নিত করি। তারপর আমরা পাস করা যেকোনো URL ফেচ করার জন্য trpl::get
ফাংশনটি ব্যবহার করি এবং রেসপন্সের জন্য অপেক্ষা করতে await
কিওয়ার্ড যোগ করি। রেসপন্সের টেক্সট পেতে, আমরা এর text
মেথড কল করি, এবং আবারও await
কিওয়ার্ড দিয়ে এটির জন্য অপেক্ষা করি। এই দুটি ধাপই অ্যাসিঙ্ক্রোনাস। get
ফাংশনের জন্য, আমাদের server-এর রেসপন্সের প্রথম অংশ পাঠানোর জন্য অপেক্ষা করতে হবে, যার মধ্যে HTTP হেডার, কুকি ইত্যাদি থাকবে এবং যা রেসপন্স বডি থেকে আলাদাভাবে সরবরাহ করা যেতে পারে। বিশেষ করে যদি বডি খুব বড় হয়, তবে এর সবটা আসতে কিছুটা সময় লাগতে পারে। যেহেতু আমাদের রেসপন্সের সম্পূর্ণটা আসার জন্য অপেক্ষা করতে হবে, তাই text
মেথডটিও async।
আমাদের এই দুটি future-কেই স্পষ্টভাবে await করতে হবে, কারণ রাস্টে future-গুলি lazy: আপনি await
কিওয়ার্ড দিয়ে তাদের কাজ করতে না বলা পর্যন্ত তারা কিছুই করে না। (আসলে, আপনি যদি একটি future ব্যবহার না করেন তবে রাস্ট একটি কম্পাইলার ওয়ার্নিং দেখাবে।) এটি আপনাকে অধ্যায় ১৩-এর Processing a Series of Items With Iterators বিভাগে ইটারেটর (iterator) আলোচনার কথা মনে করিয়ে দিতে পারে। ইটারেটররা তাদের next
মেথড কল না করা পর্যন্ত কিছুই করে না—সেটি সরাসরি হোক বা for
লুপ বা map
-এর মতো মেথড ব্যবহার করে যা পর্দার আড়ালে next
ব্যবহার করে। একইভাবে, future-গুলিও আপনি স্পষ্টভাবে তাদের কাজ করতে না বলা পর্যন্ত কিছুই করে না। এই অলসতা রাস্টকে async কোড প্রয়োজন না হওয়া পর্যন্ত চালানো থেকে বিরত রাখতে সাহায্য করে।
দ্রষ্টব্য: এটি Creating a New Thread with spawn-এ
thread::spawn
ব্যবহার করার সময় আমরা আগের অধ্যায়ে যে আচরণ দেখেছিলাম তার থেকে ভিন্ন, যেখানে আমরা অন্য থ্রেডে পাস করা ক্লোজারটি অবিলম্বে চলতে শুরু করেছিল। এটি অন্যান্য অনেক ভাষা যেভাবে async-এর সাথে কাজ করে তার থেকেও ভিন্ন। কিন্তু রাস্টের জন্য তার পারফরম্যান্স গ্যারান্টি প্রদান করতে পারাটা গুরুত্বপূর্ণ, ঠিক যেমনটি ইটারেটরের ক্ষেত্রে।
একবার আমাদের কাছে response_text
এসে গেলে, আমরা Html::parse
ব্যবহার করে এটিকে Html
টাইপের একটি ইন্সট্যান্সে পার্স করতে পারি। একটি কাঁচা স্ট্রিংয়ের পরিবর্তে, আমাদের কাছে এখন একটি ডেটা টাইপ রয়েছে যা আমরা HTML-এর সাথে আরও সমৃদ্ধ ডেটা স্ট্রাকচার হিসাবে কাজ করতে ব্যবহার করতে পারি। বিশেষ করে, আমরা একটি প্রদত্ত CSS সিলেক্টরের প্রথম ইন্সট্যান্স খুঁজে পেতে select_first
মেথডটি ব্যবহার করতে পারি। "title"
স্ট্রিংটি পাস করে, আমরা ডকুমেন্টের প্রথম <title>
এলিমেন্টটি পাব, যদি একটি থাকে। যেহেতু কোনো ম্যাচিং এলিমেন্ট নাও থাকতে পারে, select_first
একটি Option<ElementRef>
রিটার্ন করে। অবশেষে, আমরা Option::map
মেথড ব্যবহার করি, যা আমাদের Option
-এর আইটেমটি উপস্থিত থাকলে তার সাথে কাজ করতে দেয়, এবং যদি না থাকে তবে কিছুই না করতে দেয়। (আমরা এখানে একটি match
এক্সপ্রেশনও ব্যবহার করতে পারতাম, কিন্তু map
বেশি ইডিয়ম্যাটিক বা প্রচলিত।) map
-কে আমরা যে ফাংশনটি সরবরাহ করি তার বডিতে, আমরা title
-এর উপর inner_html
কল করে এর কনটেন্ট পাই, যা একটি String
। সবশেষে, আমাদের কাছে একটি Option<String>
থাকে।
লক্ষ্য করুন যে রাস্টের await
কিওয়ার্ডটি আপনি যে এক্সপ্রেশনের জন্য অপেক্ষা করছেন তার পরে বসে, আগে নয়। অর্থাৎ, এটি একটি পোস্টফিক্স (postfix) কিওয়ার্ড। আপনি যদি অন্য ভাষায় async
ব্যবহার করে থাকেন তবে এটি আপনার অভ্যস্ততার থেকে ভিন্ন হতে পারে, তবে রাস্টে এটি মেথডের চেইনগুলির সাথে কাজ করা অনেক সুন্দর করে তোলে। ফলস্বরূপ, আমরা page_title
-এর বডি পরিবর্তন করে trpl::get
এবং text
ফাংশন কলগুলিকে তাদের মধ্যে await
দিয়ে একসাথে চেইন করতে পারি, যেমনটি লিস্টিং ১৭-২ এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use trpl::Html; fn main() { // TODO: we'll add this next! } async fn page_title(url: &str) -> Option<String> { let response_text = trpl::get(url).await.text().await; Html::parse(&response_text) .select_first("title") .map(|title| title.inner_html()) }
এর সাথে, আমরা সফলভাবে আমাদের প্রথম async ফাংশন লিখে ফেলেছি! এটিকে কল করার জন্য main
-এ কিছু কোড যোগ করার আগে, চলুন আমরা যা লিখেছি এবং এর অর্থ কী তা নিয়ে আরও একটু কথা বলি।
যখন রাস্ট async
কিওয়ার্ড দিয়ে চিহ্নিত একটি ব্লক দেখে, তখন এটি এটিকে একটি অনন্য, নামহীন ডেটা টাইপে কম্পাইল করে যা Future
trait প্রয়োগ করে। যখন রাস্ট async
দিয়ে চিহ্নিত একটি ফাংশন দেখে, তখন এটি এটিকে একটি নন-async ফাংশনে কম্পাইল করে যার বডি একটি async ব্লক। একটি async ফাংশনের রিটার্ন টাইপ হলো কম্পাইলার সেই async ব্লকের জন্য যে নামহীন ডেটা টাইপ তৈরি করে তার টাইপ।
সুতরাং, async fn
লেখা একটি ফাংশন লেখার সমতুল্য যা রিটার্ন টাইপের একটি future রিটার্ন করে। কম্পাইলারের কাছে, লিস্টিং ১৭-১-এর async fn page_title
-এর মতো একটি ফাংশন সংজ্ঞা একটি নন-async ফাংশনের সমতুল্য যা এইভাবে সংজ্ঞায়িত:
#![allow(unused)] fn main() { extern crate trpl; // required for mdbook test use std::future::Future; use trpl::Html; fn page_title(url: &str) -> impl Future<Output = Option<String>> { async move { let text = trpl::get(url).await.text().await; Html::parse(&text) .select_first("title") .map(|title| title.inner_html()) } } }
আসুন রূপান্তরিত সংস্করণের প্রতিটি অংশ পর্যালোচনা করি:
- এটি
impl Trait
সিনট্যাক্স ব্যবহার করে যা আমরা অধ্যায় ১০-এর "Traits as Parameters" বিভাগে আলোচনা করেছি। - রিটার্ন করা trait টি হলো একটি
Future
যার একটি অ্যাসোসিয়েটেড টাইপOutput
রয়েছে। লক্ষ্য করুন যেOutput
টাইপটি হলোOption<String>
, যাpage_title
-এরasync fn
সংস্করণ থেকে মূল রিটার্ন টাইপের সমান। - মূল ফাংশনের বডিতে কল করা সমস্ত কোড একটি
async move
ব্লকে মোড়ানো হয়েছে। মনে রাখবেন যে ব্লকগুলি এক্সপ্রেশন। এই পুরো ব্লকটিই ফাংশন থেকে রিটার্ন করা এক্সপ্রেশন। - এই async ব্লকটি
Option<String>
টাইপের একটি মান তৈরি করে, যেমনটি এইমাত্র বর্ণনা করা হয়েছে। সেই মানটি রিটার্ন টাইপেরOutput
টাইপের সাথে মেলে। এটি আপনার দেখা অন্যান্য ব্লকের মতোই। - নতুন ফাংশন বডিটি একটি
async move
ব্লক কারণ এটিurl
প্যারামিটারটি যেভাবে ব্যবহার করে তার জন্য। (আমরা অধ্যায়ের পরবর্তীতেasync
বনামasync move
সম্পর্কে আরও অনেক কিছু আলোচনা করব।)
এখন আমরা main
-এ page_title
কল করতে পারি।
একটিমাত্র পেজের টাইটেল নির্ধারণ করা
শুরু করার জন্য, আমরা কেবল একটি পেজের টাইটেল আনব। লিস্টিং ১৭-৩ এ, আমরা Accepting Command Line Arguments বিভাগে কমান্ড লাইন আর্গুমেন্ট পাওয়ার জন্য অধ্যায় ১২-তে ব্যবহৃত একই প্যাটার্ন অনুসরণ করি। তারপর আমরা প্রথম URL টি page_title
-কে পাস করি এবং ফলাফলের জন্য await করি। যেহেতু future দ্বারা উৎপাদিত মানটি একটি Option<String>
, তাই পেজটিতে <title>
ছিল কিনা তা বিবেচনা করে বিভিন্ন বার্তা প্রিন্ট করার জন্য আমরা একটি match
এক্সপ্রেশন ব্যবহার করি।
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
দুর্ভাগ্যবশত, এই কোডটি কম্পাইল হয় না। একমাত্র যে জায়গায় আমরা await
কিওয়ার্ড ব্যবহার করতে পারি তা হলো async ফাংশন বা ব্লকে, এবং রাস্ট আমাদের বিশেষ main
ফাংশনকে async
হিসাবে চিহ্নিত করতে দেবে না।
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
main
-কে async
হিসাবে চিহ্নিত করা যায় না কারণ async কোডের একটি রানটাইম (runtime) প্রয়োজন: একটি রাস্ট crate যা অ্যাসিঙ্ক্রোনাস কোড চালানোর বিবরণ পরিচালনা করে। একটি প্রোগ্রামের main
ফাংশন একটি রানটাইম ইনিশিয়ালাইজ করতে পারে, কিন্তু এটি নিজে একটি রানটাইম নয়। (আমরা কিছুক্ষণ পরে দেখব কেন এটি এমন)। প্রতিটি রাস্ট প্রোগ্রাম যা async কোড চালায় তার অন্তত একটি জায়গা থাকে যেখানে এটি একটি রানটাইম সেট আপ করে এবং future-গুলি চালায়।
বেশিরভাগ ভাষা যা async সমর্থন করে তারা একটি রানটাইম বান্ডিল করে, কিন্তু রাস্ট তা করে না। পরিবর্তে, অনেক বিভিন্ন async রানটাইম উপলব্ধ রয়েছে, যার প্রত্যেকটি তাদের লক্ষ্য করা ব্যবহারের ক্ষেত্রে উপযুক্ত বিভিন্ন ট্রেড-অফ করে। উদাহরণস্বরূপ, অনেক সিপিইউ কোর এবং প্রচুর পরিমাণে র্যাম সহ একটি উচ্চ-থ্রুপুট ওয়েব সার্ভারের চাহিদা একটি একক কোর, অল্প পরিমাণে র্যাম এবং কোনো হিপ অ্যালোকেশন ক্ষমতা ছাড়াই একটি মাইক্রোকন্ট্রোলারের থেকে খুব আলাদা। যে crate-গুলি সেই রানটাইমগুলি সরবরাহ করে সেগুলি প্রায়শই ফাইল বা নেটওয়ার্ক I/O-এর মতো সাধারণ কার্যকারিতার async সংস্করণ সরবরাহ করে।
এখানে, এবং এই অধ্যায়ের বাকি অংশে, আমরা trpl
crate থেকে run
ফাংশনটি ব্যবহার করব, যা একটি আর্গুমেন্ট হিসাবে একটি future নেয় এবং এটিকে সম্পূর্ণ না হওয়া পর্যন্ত চালায়। পর্দার আড়ালে, run
কল করা একটি রানটাইম সেট আপ করে যা পাস করা future টি চালানোর জন্য ব্যবহৃত হয়। একবার future টি সম্পূর্ণ হলে, run
future টি যে মানটি তৈরি করেছে তা রিটার্ন করে।
আমরা page_title
দ্বারা রিটার্ন করা future টি সরাসরি run
-কে পাস করতে পারতাম, এবং এটি সম্পূর্ণ হলে, আমরা ফলাফলের Option<String>
-এর উপর ম্যাচ করতে পারতাম, যেমনটি আমরা লিস্টিং ১৭-৩-এ করার চেষ্টা করেছি। যাইহোক, অধ্যায়ের বেশিরভাগ উদাহরণের জন্য (এবং বাস্তব বিশ্বের বেশিরভাগ async কোডের জন্য), আমরা কেবল একটি async ফাংশন কলের চেয়ে বেশি কিছু করব, তাই পরিবর্তে আমরা একটি async
ব্লক পাস করব এবং page_title
কলের ফলাফলটি স্পষ্টভাবে await করব, যেমনটি লিস্টিং ১৭-৪-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
যখন আমরা এই কোডটি চালাই, তখন আমরা প্রাথমিকভাবে যে আচরণটি আশা করেছিলাম তা পাই:
$ cargo run -- https://www.rust-lang.org
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
যাক বাবা—আমরা অবশেষে কিছু কার্যকরী async কোড পেয়েছি! কিন্তু দুটি সাইটকে একে অপরের বিরুদ্ধে রেস করানোর কোড যোগ করার আগে, আসুন future-গুলি কীভাবে কাজ করে সেদিকে সংক্ষেপে আমাদের মনোযোগ ফিরিয়ে আনি।
প্রতিটি await পয়েন্ট—অর্থাৎ, প্রতিটি জায়গা যেখানে কোড await
কিওয়ার্ড ব্যবহার করে—একটি এমন স্থানকে প্রতিনিধিত্ব করে যেখানে নিয়ন্ত্রণ রানটাইমের কাছে ফিরিয়ে দেওয়া হয়। এটি কাজ করানোর জন্য, রাস্টকে async ব্লকের সাথে জড়িত অবস্থার ট্র্যাক রাখতে হবে যাতে রানটাইম অন্য কোনো কাজ শুরু করতে পারে এবং তারপর যখন এটি প্রথমটিকে আবার এগিয়ে নিয়ে যাওয়ার জন্য প্রস্তুত হয় তখন ফিরে আসতে পারে। এটি একটি অদৃশ্য স্টেট মেশিন, যেন আপনি প্রতিটি await পয়েন্টে বর্তমান অবস্থা সংরক্ষণ করার জন্য এইরকম একটি enum লিখেছেন:
#![allow(unused)] fn main() { extern crate trpl; // required for mdbook test enum PageTitleFuture<'a> { Initial { url: &'a str }, GetAwaitPoint { url: &'a str }, TextAwaitPoint { response: trpl::Response }, } }
প্রতিটি অবস্থার মধ্যে রূপান্তরের জন্য হাতে করে কোড লেখা ক্লান্তিকর এবং ত্রুটিপূর্ণ হবে, বিশেষ করে যখন আপনাকে পরে কোডে আরও কার্যকারিতা এবং আরও অবস্থা যোগ করতে হবে। ভাগ্যক্রমে, রাস্ট কম্পাইলার স্বয়ংক্রিয়ভাবে async কোডের জন্য স্টেট মেশিন ডেটা স্ট্রাকচার তৈরি এবং পরিচালনা করে। ডেটা স্ট্রাকচারের আশেপাশে স্বাভাবিক borrowing এবং ownership নিয়মগুলি সবই এখনও প্রযোজ্য, এবং আনন্দের বিষয়, কম্পাইলার সেগুলি আমাদের জন্য পরীক্ষা করে এবং দরকারী ত্রুটি বার্তা প্রদান করে। আমরা অধ্যায়ের পরে সেগুলির কয়েকটি নিয়ে কাজ করব।
শেষ পর্যন্ত, কিছু একটাকে এই স্টেট মেশিনটি চালাতে হবে, এবং সেই কিছু একটা হলো একটি রানটাইম। (এই কারণেই আপনি রানটাইম নিয়ে খোঁজ করার সময় এক্সিকিউটর (executors) এর উল্লেখ পেতে পারেন: একটি এক্সিকিউটর হলো একটি রানটাইমের অংশ যা async কোড চালানোর জন্য দায়ী।)
এখন আপনি দেখতে পাচ্ছেন কেন কম্পাইলার আমাদের লিস্টিং ১৭-৩-এ main
-কে নিজে একটি async ফাংশন তৈরি করতে বাধা দিয়েছিল। যদি main
একটি async ফাংশন হতো, তবে main
যে future টি রিটার্ন করত তার স্টেট মেশিন পরিচালনা করার জন্য অন্য কিছুর প্রয়োজন হতো, কিন্তু main
হলো প্রোগ্রামের সূচনা বিন্দু! পরিবর্তে, আমরা main
-এ trpl::run
ফাংশনটি কল করেছি একটি রানটাইম সেট আপ করতে এবং async
ব্লকের দ্বারা রিটার্ন করা future টি শেষ না হওয়া পর্যন্ত চালাতে।
দ্রষ্টব্য: কিছু রানটাইম ম্যাক্রো সরবরাহ করে যাতে আপনি একটি async
main
ফাংশন লিখতে পারেন। সেই ম্যাক্রোগুলিasync fn main() { ... }
-কে একটি সাধারণfn main
-এ পুনর্লিখন করে, যা আমরা লিস্টিং ১৭-৪-এ হাতে করে যা করেছি তাই করে: একটি ফাংশন কল করে যা একটি future-কেtrpl::run
-এর মতো সম্পূর্ণ না হওয়া পর্যন্ত চালায়।
এখন আসুন এই অংশগুলি একসাথে রাখি এবং দেখি আমরা কীভাবে কনকারেন্ট কোড লিখতে পারি।
আমাদের দুটি URL-কে একে অপরের বিরুদ্ধে রেস করানো
লিস্টিং ১৭-৫-এ, আমরা কমান্ড লাইন থেকে পাস করা দুটি ভিন্ন URL দিয়ে page_title
কল করি এবং তাদের রেস করাই।
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
আমরা ব্যবহারকারী-প্রদত্ত প্রতিটি URL-এর জন্য page_title
কল করে শুরু করি। আমরা ফলাফলস্বরূপ future-গুলিকে title_fut_1
এবং title_fut_2
হিসাবে সংরক্ষণ করি। মনে রাখবেন, এগুলি এখনও কিছুই করে না, কারণ future-গুলি অলস এবং আমরা এখনও তাদের await করিনি। তারপর আমরা future-গুলিকে trpl::race
-এ পাস করি, যা একটি মান রিটার্ন করে নির্দেশ করে যে পাস করা future-গুলির মধ্যে কোনটি প্রথমে শেষ হয়।
দ্রষ্টব্য: পর্দার আড়ালে,
race
একটি আরও সাধারণ ফাংশন,select
-এর উপর নির্মিত, যা আপনি বাস্তব-বিশ্বের রাস্ট কোডে আরও প্রায়ই দেখতে পাবেন। একটিselect
ফাংশন এমন অনেক কিছু করতে পারে যাtrpl::race
ফাংশনটি করতে পারে না, তবে এর কিছু অতিরিক্ত জটিলতাও রয়েছে যা আমরা আপাতত এড়িয়ে যেতে পারি।
যেকোনো future আইনসম্মতভাবে "জিততে" পারে, তাই একটি Result
রিটার্ন করার কোনো মানে হয় না। পরিবর্তে, race
এমন একটি টাইপ রিটার্ন করে যা আমরা আগে দেখিনি, trpl::Either
। Either
টাইপটি একটি Result
-এর সাথে কিছুটা সাদৃশ্যপূর্ণ কারণ এর দুটি কেস রয়েছে। Result
-এর মতো নয়, Either
-এ সাফল্য বা ব্যর্থতার কোনো ধারণা নেই। পরিবর্তে, এটি "একটি বা অন্যটি" নির্দেশ করতে Left
এবং Right
ব্যবহার করে:
#![allow(unused)] fn main() { enum Either<A, B> { Left(A), Right(B), } }
race
ফাংশনটি Left
রিটার্ন করে প্রথম ফিউচার আর্গুমেন্টের আউটপুট সহ যদি সেটি প্রথমে শেষ হয়, অথবা Right
রিটার্ন করে দ্বিতীয় ফিউচার আর্গুমেন্টের আউটপুট সহ যদি সেটি প্রথমে শেষ হয়। এটি ফাংশন কল করার সময় আর্গুমেন্টগুলির ক্রমের সাথে মিলে যায়: প্রথম আর্গুমেন্টটি দ্বিতীয় আর্গুমেন্টের বাম দিকে থাকে।
আমরা page_title
-কে এমনভাবে আপডেট করি যাতে এটি পাস করা একই URL রিটার্ন করে। এইভাবে, যদি প্রথমে রিটার্ন করা পেজটির কোনো <title>
না থাকে যা আমরা সমাধান করতে পারি, আমরা তবুও একটি অর্থপূর্ণ বার্তা প্রিন্ট করতে পারি। সেই তথ্য উপলব্ধ থাকায়, আমরা আমাদের println!
আউটপুট আপডেট করে শেষ করি যাতে কোন URL প্রথমে শেষ হয়েছে এবং সেই URL-এর ওয়েব পেজের <title>
কী, যদি থাকে, তা উভয়ই নির্দেশ করা যায়।
আপনি এখন একটি ছোট কার্যকরী ওয়েব স্ক্র্যাপার তৈরি করেছেন! কয়েকটি URL বেছে নিন এবং কমান্ড লাইন টুলটি চালান। আপনি আবিষ্কার করতে পারেন যে কিছু সাইট ধারাবাহিকভাবে অন্যদের চেয়ে দ্রুত, আবার অন্য ক্ষেত্রে দ্রুততর সাইটটি রান থেকে রানে পরিবর্তিত হয়। আরও গুরুত্বপূর্ণভাবে, আপনি future-এর সাথে কাজ করার মূল বিষয়গুলি শিখেছেন, তাই এখন আমরা async দিয়ে কী করতে পারি সে সম্পর্কে আরও গভীরে যেতে পারি।