অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং-এর মূল বিষয়: Async, Await, Futures, এবং Streams
আমরা কম্পিউটারকে যে কাজগুলি করতে বলি তার অনেকগুলি শেষ হতে বেশ কিছুটা সময় নিতে পারে। এই দীর্ঘ-চলমান প্রক্রিয়াগুলি সম্পূর্ণ হওয়ার জন্য অপেক্ষা করার সময় আমরা যদি অন্য কিছু করতে পারতাম তবে ভাল হত। আধুনিক কম্পিউটারগুলি একবারে একাধিক অপারেশনে কাজ করার জন্য দুটি কৌশল সরবরাহ করে: প্যারালেলিজম (parallelism) এবং কনকারেন্সি (concurrency)। একবার যখন আমরা প্যারালাল বা কনকারেন্ট অপারেশন যুক্ত প্রোগ্রাম লেখা শুরু করি, তখন আমরা দ্রুত অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং-এর অন্তর্নিহিত নতুন চ্যালেঞ্জগুলির সম্মুখীন হই, যেখানে অপারেশনগুলি শুরু হওয়ার ক্রমে ধারাবাহিকভাবে শেষ নাও হতে পারে। এই চ্যাপ্টারটি প্যারালেলিজম এবং কনকারেন্সির জন্য থ্রেড ব্যবহারের বিষয়ে Chapter 16-এর উপর ভিত্তি করে তৈরি হয়েছে এবং অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং-এর একটি বিকল্প পদ্ধতির পরিচয় দেয়: Rust-এর Futures, Streams, সেগুলিকে সমর্থন করে এমন async
এবং await
সিনট্যাক্স এবং অ্যাসিঙ্ক্রোনাস অপারেশনগুলির মধ্যে পরিচালনা ও সমন্বয় করার টুল।
আসুন একটি উদাহরণ বিবেচনা করা যাক। ধরুন আপনি একটি পারিবারিক উদযাপনের একটি ভিডিও এক্সপোর্ট করছেন, এমন একটি অপারেশন যা কয়েক মিনিট থেকে কয়েক ঘন্টা পর্যন্ত সময় নিতে পারে। ভিডিও এক্সপোর্ট যতটা সম্ভব CPU এবং GPU পাওয়ার ব্যবহার করবে। যদি আপনার কেবল একটি CPU কোর থাকত এবং আপনার অপারেটিং সিস্টেম সেই এক্সপোর্টটি সম্পূর্ণ না হওয়া পর্যন্ত এটিকে স্থগিত না করত—অর্থাৎ, যদি এটি এক্সপোর্টটিকে synchronously এক্সিকিউট করত—তাহলে সেই কাজটি চলার সময় আপনি আপনার কম্পিউটারে অন্য কিছু করতে পারতেন না। এটি একটি হতাশাজনক অভিজ্ঞতা হত। সৌভাগ্যবশত, আপনার কম্পিউটারের অপারেটিং সিস্টেম অন্যান্য কাজগুলিকে যুগপতভাবে সম্পন্ন করার সুযোগ দিতে যথেষ্ট পরিমাণে এক্সপোর্টকে অদৃশ্যভাবে বাধা দিতে পারে এবং দেয়।
এখন ধরুন আপনি অন্য কারও শেয়ার করা একটি ভিডিও ডাউনলোড করছেন, যেটিতেও কিছুক্ষণ সময় লাগতে পারে কিন্তু এটি CPU-এর বেশি সময় নেয় না। এই ক্ষেত্রে, CPU-কে নেটওয়ার্ক থেকে ডেটা আসার জন্য অপেক্ষা করতে হবে। আপনি ডেটা আসা শুরু করার সাথে সাথেই পড়া শুরু করতে পারলেও, সমস্ত ডেটা আসতে কিছুটা সময় লাগতে পারে। এমনকি সমস্ত ডেটা উপস্থিত থাকলেও, যদি ভিডিওটি বেশ বড় হয়, তবে এটি লোড হতে কমপক্ষে এক বা দুই সেকেন্ড সময় লাগতে পারে। এটি খুব বেশি মনে নাও হতে পারে, তবে এটি একটি আধুনিক প্রসেসরের জন্য অনেক দীর্ঘ সময়, যা প্রতি সেকেন্ডে কয়েক বিলিয়ন অপারেশন সম্পাদন করতে পারে। আবারও, আপনার অপারেটিং সিস্টেম নেটওয়ার্ক কলের কাজ শেষ হওয়ার অপেক্ষার সময় CPU-কে অন্য কাজ করার অনুমতি দেওয়ার জন্য আপনার প্রোগ্রামটিকে অদৃশ্যভাবে বাধা দেবে।
ভিডিও এক্সপোর্ট হল একটি CPU-bound বা compute-bound অপারেশনের উদাহরণ। এটি CPU বা GPU-এর মধ্যে কম্পিউটারের সম্ভাব্য ডেটা প্রসেসিং গতি এবং সেই গতির কতটা এটি অপারেশনে ডেডিকেট করতে পারে তার দ্বারা সীমাবদ্ধ। ভিডিও ডাউনলোড হল একটি IO-bound অপারেশনের উদাহরণ, কারণ এটি কম্পিউটারের input এবং output-এর গতির দ্বারা সীমাবদ্ধ; এটি কেবল ততটাই দ্রুত চলতে পারে যতটা দ্রুত নেটওয়ার্ক জুড়ে ডেটা পাঠানো যায়।
এই উভয় উদাহরণেই, অপারেটিং সিস্টেমের অদৃশ্য বাধাগুলি এক ধরনের কনকারেন্সি সরবরাহ করে। সেই কনকারেন্সি অবশ্য কেবল সম্পূর্ণ প্রোগ্রামের স্তরে ঘটে: অপারেটিং সিস্টেম অন্য প্রোগ্রামগুলিকে কাজ করার সুযোগ দেওয়ার জন্য একটি প্রোগ্রামকে বাধা দেয়। অনেক ক্ষেত্রে, যেহেতু আমরা অপারেটিং সিস্টেমের চেয়ে অনেক বেশি সুক্ষ্ম স্তরে আমাদের প্রোগ্রামগুলি বুঝতে পারি, তাই আমরা কনকারেন্সির এমন সুযোগগুলি দেখতে পাই যা অপারেটিং সিস্টেম দেখতে পায় না।
উদাহরণস্বরূপ, যদি আমরা ফাইল ডাউনলোড পরিচালনা করার জন্য একটি টুল তৈরি করি, তাহলে আমাদের প্রোগ্রামটি এমনভাবে লিখতে পারা উচিত যাতে একটি ডাউনলোড শুরু করলে UI লক না হয় এবং ব্যবহারকারীরা একই সাথে একাধিক ডাউনলোড শুরু করতে পারে। নেটওয়ার্কের সাথে ইন্টারঅ্যাক্ট করার জন্য অনেকগুলি অপারেটিং সিস্টেম API blocking, যদিও; অর্থাৎ, তারা প্রোগ্রামের অগ্রগতি ব্লক করে যতক্ষণ না তারা যে ডেটা প্রসেস করছে তা সম্পূর্ণরূপে প্রস্তুত হয়।
Note: আপনি যদি এটি সম্পর্কে চিন্তা করেন তবে বেশিরভাগ ফাংশন কল এইভাবে কাজ করে। যাইহোক, blocking শব্দটি সাধারণত ফাইল, নেটওয়ার্ক বা কম্পিউটারের অন্যান্য রিসোর্সের সাথে ইন্টারঅ্যাক্ট করা ফাংশন কলগুলির জন্য সংরক্ষিত থাকে, কারণ সেই ক্ষেত্রগুলিতে একটি individual প্রোগ্রাম non-blocking অপারেশনে উপকৃত হবে।
আমরা প্রতিটি ফাইল ডাউনলোড করার জন্য একটি ডেডিকেটেড থ্রেড স্পন করে আমাদের main থ্রেডকে ব্লক করা এড়াতে পারি। যাইহোক, সেই থ্রেডগুলির ওভারহেড অবশেষে একটি সমস্যা হয়ে দাঁড়াবে। কলের শুরুতেই যদি ব্লক না করে তবে সেটি আরও ভাল হত। আমরা যদি ব্লকিং কোডে ব্যবহার করা একই ডিরেক্ট স্টাইলে লিখতে পারতাম, তাহলে আরও ভাল হত, অনেকটা এইরকম:
let data = fetch_data_from(url).await;
println!("{data}");
Rust-এর async ( asynchronous-এর সংক্ষিপ্ত) অ্যাবস্ট্রাকশন আমাদের ঠিক সেটাই দেয়। এই চ্যাপ্টারে, আপনি নিম্নলিখিত বিষয়গুলি কভার করার সাথে সাথে async সম্পর্কে সমস্ত কিছু শিখবেন:
- কীভাবে Rust-এর
async
এবংawait
সিনট্যাক্স ব্যবহার করবেন - Chapter 16-এ আমরা যে চ্যালেঞ্জগুলি দেখেছিলাম তার কিছু সমাধান করতে কীভাবে async মডেল ব্যবহার করবেন
- কীভাবে multithreading এবং async একে অপরের পরিপূরক সমাধান সরবরাহ করে, যা আপনি অনেক ক্ষেত্রে একত্রিত করতে পারেন
বাস্তবে async কীভাবে কাজ করে তা দেখার আগে, আমাদের প্যারালেলিজম এবং কনকারেন্সির মধ্যে পার্থক্যগুলি নিয়ে আলোচনা করার জন্য একটি সংক্ষিপ্ত বিরতি নিতে হবে।
প্যারালেলিজম এবং কনকারেন্সি (Parallelism and Concurrency)
আমরা ఇప్పటి পর্যন্ত প্যারালেলিজম এবং কনকারেন্সিকে বেশিরভাগ ক্ষেত্রে বিনিময়যোগ্য হিসাবে বিবেচনা করেছি। এখন আমাদের তাদের মধ্যে আরও সুনির্দিষ্টভাবে পার্থক্য করতে হবে, কারণ আমরা কাজ শুরু করার সাথে সাথে পার্থক্যগুলি দেখা যাবে।
একটি সফ্টওয়্যার প্রোজেক্টে একটি দল কীভাবে কাজ ভাগ করতে পারে তার বিভিন্ন উপায় বিবেচনা করুন। আপনি একজন individual সদস্যকে একাধিক কাজ বরাদ্দ করতে পারেন, প্রতিটি সদস্যকে একটি কাজ বরাদ্দ করতে পারেন, বা দুটি পদ্ধতির মিশ্রণ ব্যবহার করতে পারেন।
যখন একজন ব্যক্তি কোনও কাজ সম্পূর্ণ হওয়ার আগেই বেশ কয়েকটি ভিন্ন কাজ নিয়ে কাজ করেন, তখন এটি হল কনকারেন্সি। হতে পারে আপনার কম্পিউটারে দুটি ভিন্ন প্রোজেক্ট চেক আউট করা আছে, এবং যখন আপনি একটি প্রোজেক্টে বিরক্ত হন বা আটকে যান, তখন আপনি অন্যটিতে স্যুইচ করেন। আপনি কেবল একজন ব্যক্তি, তাই আপনি একই সময়ে উভয় কাজেই অগ্রগতি করতে পারবেন না, তবে আপনি মাল্টি-টাস্ক করতে পারেন, তাদের মধ্যে স্যুইচ করে একবারে একটিতে অগ্রগতি করতে পারেন (চিত্র 17-1 দেখুন)।
যখন দলটি প্রতিটি সদস্যকে একটি করে কাজ নেয় এবং একা একা কাজ করে, তখন এটি প্যারালেলিজম। দলের প্রত্যেকে একই সময়ে অগ্রগতি করতে পারে (চিত্র 17-2 দেখুন)।
এই উভয় ওয়ার্কফ্লোতেই, আপনাকে বিভিন্ন কাজের মধ্যে সমন্বয় করতে হতে পারে। হতে পারে আপনি ভেবেছিলেন একজন ব্যক্তিকে দেওয়া কাজটি অন্য সবার কাজ থেকে সম্পূর্ণ স্বাধীন, কিন্তু আসলে এটির জন্য দলের অন্য একজন ব্যক্তির প্রথমে তাদের কাজটি শেষ করা প্রয়োজন। কিছু কাজ প্যারালালে করা যেতে পারে, তবে এর মধ্যে কিছু আসলে সিরিয়াল: এটি কেবল একটি সিরিজে ঘটতে পারে, একের পর এক কাজ, যেমন চিত্র 17-3-এ।
একইভাবে, আপনি বুঝতে পারেন যে আপনার নিজের একটি কাজ আপনার অন্য একটি কাজের উপর নির্ভরশীল। এখন আপনার কনকারেন্ট কাজও সিরিয়াল হয়ে গেছে।
প্যারালেলিজম এবং কনকারেন্সি একে অপরের সাথে ছেদ করতে পারে। আপনি যদি জানতে পারেন যে আপনার একজন সহকর্মী আপনার একটি কাজ শেষ না করা পর্যন্ত আটকে আছেন, তাহলে আপনি সম্ভবত আপনার সহকর্মীকে "আনব্লক" করার জন্য সেই কাজের উপর সমস্ত মনোযোগ দেবেন। আপনি এবং আপনার সহকর্মী আর প্যারালালে কাজ করতে পারবেন না, এবং আপনি আপনার নিজের কাজগুলিতেও আর কনকারেন্টলি কাজ করতে পারবেন না।
সফ্টওয়্যার এবং হার্ডওয়্যারের ক্ষেত্রেও একই বেসিক ডায়নামিকগুলি কার্যকর হয়। একটি single CPU কোর সহ একটি মেশিনে, CPU একবারে কেবল একটি অপারেশন সম্পাদন করতে পারে, তবে এটি এখনও কনকারেন্টলি কাজ করতে পারে। থ্রেড, প্রসেস এবং async-এর মতো টুল ব্যবহার করে, কম্পিউটার একটি অ্যাক্টিভিটি পজ করতে পারে এবং অন্যগুলিতে স্যুইচ করতে পারে, অবশেষে আবার সেই প্রথম অ্যাক্টিভিটিতে ফিরে আসার আগে। একাধিক CPU কোর সহ একটি মেশিনে, এটি প্যারালালে কাজও করতে পারে। একটি কোর একটি কাজ সম্পাদন করতে পারে যখন অন্য কোর সম্পূর্ণ সম্পর্কহীন একটি কাজ সম্পাদন করে এবং সেই অপারেশনগুলি আসলে একই সময়ে ঘটে।
Rust-এ async নিয়ে কাজ করার সময়, আমরা সবসময় কনকারেন্সির সাথে কাজ করি। হার্ডওয়্যার, অপারেটিং সিস্টেম এবং আমরা যে async রানটাইম ব্যবহার করছি তার উপর নির্ভর করে (async রানটাইম সম্পর্কে শীঘ্রই আরও আলোচনা করা হবে), সেই কনকারেন্সি হুডের নিচে প্যারালেলিজমও ব্যবহার করতে পারে।
এখন, আসুন Rust-এ অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং কীভাবে কাজ করে তাতে ডুব দেওয়া যাক।