সবকিছু একসাথে: ফিউচার, টাস্ক এবং থ্রেড (Putting It All Together: Futures, Tasks, and Threads)
আমরা যেমনটি Chapter 16-এ দেখেছি, থ্রেডগুলি কনকারেন্সির একটি পদ্ধতি সরবরাহ করে। আমরা এই চ্যাপ্টারে আরেকটি পদ্ধতি দেখেছি: ফিউচার এবং স্ট্রিম সহ অ্যাসিঙ্ক্রোনাস ব্যবহার করা। আপনি যদি ভাবছেন কখন অন্যটির চেয়ে কোন পদ্ধতি বেছে নেবেন, তাহলে উত্তর হল: এটি নির্ভর করে! এবং অনেক ক্ষেত্রে, পছন্দটি থ্রেড বা অ্যাসিঙ্ক্রোনাস নয়, বরং থ্রেড এবং অ্যাসিঙ্ক্রোনাস।
অনেক অপারেটিং সিস্টেম এখন কয়েক দশক ধরে থ্রেডিং-ভিত্তিক কনকারেন্সি মডেল সরবরাহ করে আসছে এবং ফলস্বরূপ অনেক প্রোগ্রামিং ভাষা তাদের সমর্থন করে। যাইহোক, এই মডেলগুলি তাদের ট্রেডঅফ ছাড়া নয়। অনেক অপারেটিং সিস্টেমে, তারা প্রতিটি থ্রেডের জন্য বেশ কিছুটা মেমরি ব্যবহার করে এবং সেগুলি শুরু এবং বন্ধ করার জন্য কিছু ওভারহেড সহ আসে। থ্রেডগুলি তখনই একটি অপশন যখন আপনার অপারেটিং সিস্টেম এবং হার্ডওয়্যার তাদের সমর্থন করে। মূলধারার ডেস্কটপ এবং মোবাইল কম্পিউটারগুলির বিপরীতে, কিছু এমবেডেড সিস্টেমে কোনও OS নেই, তাই তাদের থ্রেডও নেই।
অ্যাসিঙ্ক্রোনাস মডেলটি ট্রেডঅফের একটি ভিন্ন—এবং চূড়ান্তভাবে পরিপূরক—সেট সরবরাহ করে। অ্যাসিঙ্ক্রোনাস মডেলে, কনকারেন্ট অপারেশনগুলির জন্য তাদের নিজস্ব থ্রেডের প্রয়োজন হয় না। পরিবর্তে, তারা টাস্কগুলিতে চলতে পারে, যেমনটি আমরা স্ট্রিম বিভাগে একটি সিঙ্ক্রোনাস ফাংশন থেকে কাজ শুরু করতে trpl::spawn_task
ব্যবহার করার সময় দেখেছি। একটি টাস্ক একটি থ্রেডের মতোই, কিন্তু অপারেটিং সিস্টেম দ্বারা পরিচালিত হওয়ার পরিবর্তে, এটি লাইব্রেরি-লেভেল কোড দ্বারা পরিচালিত হয়: রানটাইম।
পূর্ববর্তী বিভাগে, আমরা দেখেছি যে আমরা একটি অ্যাসিঙ্ক্রোনাস চ্যানেল ব্যবহার করে এবং একটি অ্যাসিঙ্ক্রোনাস টাস্ক স্পন করে একটি স্ট্রিম তৈরি করতে পারি যেটিকে আমরা সিঙ্ক্রোনাস কোড থেকে কল করতে পারি। আমরা একটি থ্রেড দিয়ে ঠিক একই কাজ করতে পারি। Listing 17-40-এ, আমরা trpl::spawn_task
এবং trpl::sleep
ব্যবহার করেছি। Listing 17-41-এ, আমরা সেগুলিকে get_intervals
ফাংশনে স্ট্যান্ডার্ড লাইব্রেরি থেকে thread::spawn
এবং thread::sleep
API দিয়ে প্রতিস্থাপন করি।
extern crate trpl; // required for mdbook test use std::{pin::pin, thread, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval #{count}")) .throttle(Duration::from_millis(500)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(item) => println!("{item}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; if let Err(send_error) = tx.send(format!("Message: '{message}'")) { eprintln!("Cannot send message '{message}': {send_error}"); break; } } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); // This is *not* `trpl::spawn` but `std::thread::spawn`! thread::spawn(move || { let mut count = 0; loop { // Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`! thread::sleep(Duration::from_millis(1)); count += 1; if let Err(send_error) = tx.send(count) { eprintln!("Could not send interval {count}: {send_error}"); break; }; } }); ReceiverStream::new(rx) }
আপনি যদি এই কোডটি চালান, তাহলে আউটপুটটি Listing 17-40-এর মতোই হবে। এবং লক্ষ্য করুন কলিং কোডের দৃষ্টিকোণ থেকে এখানে কতটা সামান্য পরিবর্তন হয়েছে। আরও কী, যদিও আমাদের ফাংশনগুলির মধ্যে একটি রানটাইমে একটি অ্যাসিঙ্ক্রোনাস টাস্ক স্পন করেছে এবং অন্যটি একটি OS থ্রেড স্পন করেছে, ফলাফলের স্ট্রিমগুলি পার্থক্য দ্বারা প্রভাবিত হয়নি।
তাদের মিল থাকা সত্ত্বেও, এই দুটি পদ্ধতির আচরণ খুব আলাদা, যদিও আমরা এই খুব সহজ উদাহরণে এটি পরিমাপ করতে কঠিন সময় পেতে পারি। আমরা যেকোনো আধুনিক ব্যক্তিগত কম্পিউটারে কয়েক মিলিয়ন অ্যাসিঙ্ক্রোনাস টাস্ক স্পন করতে পারি। আমরা যদি থ্রেড দিয়ে এটি করার চেষ্টা করতাম, তাহলে আমরা আক্ষরিক অর্থে মেমরির বাইরে চলে যেতাম!
যাইহোক, এই API গুলি এত মিল থাকার একটি কারণ রয়েছে। থ্রেডগুলি সিঙ্ক্রোনাস অপারেশনের সেটগুলির জন্য একটি সীমানা হিসাবে কাজ করে; থ্রেডগুলির মধ্যে কনকারেন্সি সম্ভব। টাস্কগুলি অ্যাসিঙ্ক্রোনাস অপারেশনের সেটগুলির জন্য একটি সীমানা হিসাবে কাজ করে; টাস্কগুলির মধ্যে এবং ভিতরে উভয় ক্ষেত্রেই কনকারেন্সি সম্ভব, কারণ একটি টাস্ক তার বডিতে ফিউচারগুলির মধ্যে স্যুইচ করতে পারে। অবশেষে, ফিউচারগুলি হল Rust-এর কনকারেন্সির সবচেয়ে দানাদার একক এবং প্রতিটি ফিউচার অন্য ফিউচারের একটি গাছকে উপস্থাপন করতে পারে। রানটাইম—বিশেষ করে, এর এক্সিকিউটর—টাস্কগুলি পরিচালনা করে এবং টাস্কগুলি ফিউচারগুলি পরিচালনা করে। সেই ক্ষেত্রে, টাস্কগুলি হল হালকা, রানটাইম-পরিচালিত থ্রেডের মতো, অপারেটিং সিস্টেমের পরিবর্তে একটি রানটাইম দ্বারা পরিচালিত হওয়ার কারণে অতিরিক্ত ক্ষমতা সহ।
এর মানে এই নয় যে অ্যাসিঙ্ক্রোনাস টাস্কগুলি সর্বদা থ্রেডের চেয়ে ভাল (বা এর বিপরীত)। থ্রেডের সাথে কনকারেন্সি কিছু উপায়ে async
-এর সাথে কনকারেন্সির চেয়ে একটি সহজ প্রোগ্রামিং মডেল। এটি একটি শক্তি বা দুর্বলতা হতে পারে। থ্রেডগুলি কিছুটা “ফায়ার অ্যান্ড ফরগেট”; তাদের একটি ফিউচারের কোনও নেটিভ সমতুল্য নেই, তাই অপারেটিং সিস্টেম নিজেই বাধা না দেওয়া পর্যন্ত সেগুলি সম্পূর্ণ হওয়া পর্যন্ত চলে। অর্থাৎ, তাদের ইন্ট্রাটাস্ক কনকারেন্সির জন্য কোনও অন্তর্নির্মিত সমর্থন নেই যেভাবে ফিউচারগুলি করে। Rust-এ থ্রেডগুলির কোনও বাতিলকরণ প্রক্রিয়াও নেই—এমন একটি বিষয় যা আমরা এই চ্যাপ্টারে স্পষ্টভাবে কভার করিনি কিন্তু এই সত্য দ্বারা বোঝানো হয়েছিল যে আমরা যখনই একটি ফিউচার শেষ করেছি, তার স্টেট সঠিকভাবে পরিষ্কার হয়ে গেছে।
এই সীমাবদ্ধতাগুলি থ্রেডগুলিকে ফিউচারের চেয়ে কম্পোজ করা আরও কঠিন করে তোলে। উদাহরণস্বরূপ, থ্রেড ব্যবহার করে এই চ্যাপ্টারে আমরা আগে তৈরি করা timeout
এবং throttle
মেথডগুলির মতো হেল্পার তৈরি করা আরও কঠিন। ফিউচারগুলি আরও সমৃদ্ধ ডেটা স্ট্রাকচার হওয়ার অর্থ হল সেগুলিকে আরও স্বাভাবিকভাবে একসাথে কম্পোজ করা যেতে পারে, যেমনটি আমরা দেখেছি।
টাস্কগুলি, তাহলে, আমাদের ফিউচারগুলির উপর অতিরিক্ত নিয়ন্ত্রণ দেয়, যা আমাদের কোথায় এবং কীভাবে সেগুলিকে গ্রুপ করতে হবে তা বেছে নিতে দেয়। এবং এটি দেখা যাচ্ছে যে থ্রেড এবং টাস্কগুলি প্রায়শই একসাথে খুব ভাল কাজ করে, কারণ টাস্কগুলি (অন্তত কিছু রানটাইমে) থ্রেডগুলির মধ্যে ঘোরাফেরা করা যেতে পারে। প্রকৃতপক্ষে, হুডের নিচে, আমরা যে রানটাইমটি ব্যবহার করে আসছি—spawn_blocking
এবং spawn_task
ফাংশন সহ—ডিফল্টভাবে মাল্টিথ্রেডেড! অনেকগুলি রানটাইম সিস্টেমের সামগ্রিক পারফরম্যান্স উন্নত করতে, থ্রেডগুলি বর্তমানে কীভাবে ব্যবহার করা হচ্ছে তার উপর ভিত্তি করে থ্রেডগুলির মধ্যে স্বচ্ছভাবে টাস্কগুলিকে সরানোর জন্য ওয়ার্ক স্টিলিং নামক একটি পদ্ধতি ব্যবহার করে। সেই পদ্ধতির জন্য আসলে থ্রেড এবং টাস্ক এবং সেইজন্য ফিউচারের প্রয়োজন।
কখন কোন পদ্ধতি ব্যবহার করবেন তা নিয়ে চিন্তা করার সময়, এই নিয়মগুলি বিবেচনা করুন:
- যদি কাজটি খুব প্যারালাইজেবল হয়, যেমন প্রচুর ডেটা প্রসেস করা যেখানে প্রতিটি অংশ আলাদাভাবে প্রসেস করা যায়, তাহলে থ্রেডগুলি একটি ভাল পছন্দ।
- যদি কাজটি খুব কনকারেন্ট হয়, যেমন বিভিন্ন উৎস থেকে মেসেজ হ্যান্ডেল করা যা বিভিন্ন বিরতিতে বা বিভিন্ন হারে আসতে পারে, তাহলে অ্যাসিঙ্ক্রোনাস একটি ভাল পছন্দ।
এবং যদি আপনার প্যারালেলিজম এবং কনকারেন্সি উভয়েরই প্রয়োজন হয়, তাহলে আপনাকে থ্রেড এবং অ্যাসিঙ্ক্রোনাসের মধ্যে বেছে নিতে হবে না। আপনি সেগুলিকে অবাধে একসাথে ব্যবহার করতে পারেন, প্রত্যেকটিকে তার সেরা অংশটি করতে দিয়ে। উদাহরণস্বরূপ, Listing 17-42 বাস্তব-বিশ্বের Rust কোডে এই ধরনের মিশ্রণের একটি মোটামুটি সাধারণ উদাহরণ দেখায়।
extern crate trpl; // for mdbook test use std::{thread, time::Duration}; fn main() { let (tx, mut rx) = trpl::channel(); thread::spawn(move || { for i in 1..11 { tx.send(i).unwrap(); thread::sleep(Duration::from_secs(1)); } }); trpl::run(async { while let Some(message) = rx.recv().await { println!("{message}"); } }); }
আমরা একটি অ্যাসিঙ্ক্রোনাস চ্যানেল তৈরি করে শুরু করি, তারপর একটি থ্রেড স্পন করি যা চ্যানেলের সেন্ডার সাইডের ownership নেয়। থ্রেডের মধ্যে, আমরা 1 থেকে 10 পর্যন্ত সংখ্যাগুলি পাঠাই, প্রতিটিটির মধ্যে এক সেকেন্ডের জন্য স্লিপ করি। অবশেষে, আমরা একটি অ্যাসিঙ্ক্রোনাস ব্লক সহ তৈরি একটি ফিউচার চালাই যা trpl::run
-এ পাস করা হয় ঠিক যেমনটি আমরা চ্যাপ্টার জুড়ে করেছি। সেই ফিউচারে, আমরা সেই মেসেজগুলির জন্য অপেক্ষা করি, ঠিক যেমনটি আমরা দেখেছি অন্য মেসেজ-পাসিং উদাহরণগুলিতে।
আমরা যে দৃশ্যটি দিয়ে চ্যাপ্টারটি শুরু করেছি তাতে ফিরে যেতে, একটি ডেডিকেটেড থ্রেড ব্যবহার করে ভিডিও এনকোডিং টাস্কগুলির একটি সেট চালানোর কথা কল্পনা করুন (কারণ ভিডিও এনকোডিং কম্পিউট-বাউন্ড) কিন্তু সেই অপারেশনগুলি একটি অ্যাসিঙ্ক্রোনাস চ্যানেলের সাথে UI-কে জানানো হচ্ছে। বাস্তব-বিশ্বের ব্যবহারের ক্ষেত্রে এই ধরনের কম্বিনেশনের অসংখ্য উদাহরণ রয়েছে।
সারাংশ (Summary)
এই বইয়ে আপনি কনকারেন্সির শেষ দেখা পাবেন না। Chapter 21-এর প্রোজেক্টটি এখানে আলোচিত সহজ উদাহরণগুলির চেয়ে আরও বাস্তব পরিস্থিতিতে এই ধারণাগুলি প্রয়োগ করবে এবং থ্রেডিং বনাম টাস্কগুলির সাথে সমস্যা সমাধানের তুলনা আরও সরাসরি করবে।
আপনি এই পদ্ধতির মধ্যে কোনটি বেছে নিন না কেন, Rust আপনাকে নিরাপদ, দ্রুত, কনকারেন্ট কোড লেখার জন্য প্রয়োজনীয় টুল সরবরাহ করে—হোক সেটি একটি high-throughput ওয়েব সার্ভার বা একটি এমবেডেড অপারেটিং সিস্টেমের জন্য।
এরপর, আমরা আপনার Rust প্রোগ্রামগুলি বড় হওয়ার সাথে সাথে সমস্যাগুলি মডেল করার এবং সমাধানগুলিকে গঠন করার প্রচলিত উপায়গুলি সম্পর্কে কথা বলব। এছাড়াও, আমরা আলোচনা করব কিভাবে Rust-এর প্রচলিত পদ্ধতিগুলি অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং থেকে আপনার পরিচিত পদ্ধতিগুলির সাথে সম্পর্কিত।