অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের মূলনীতি: Async, Await, Futures, এবং Streams
আমরা কম্পিউটারকে এমন অনেক কাজ করতে বলি যা শেষ হতে বেশ কিছুটা সময় নিতে পারে। এই দীর্ঘ সময় ধরে চলা প্রসেসগুলো শেষ হওয়ার জন্য অপেক্ষা করার সময় যদি আমরা অন্য কিছু করতে পারতাম, তাহলে খুব ভালো হতো। আধুনিক কম্পিউটারগুলো একই সময়ে একাধিক অপারেশন নিয়ে কাজ করার জন্য দুটি কৌশল প্রদান করে: প্যারালালিসম (parallelism) এবং কনকারেন্সি (concurrency)। যখন আমরা এমন প্রোগ্রাম লিখতে শুরু করি যেখানে প্যারালাল বা কনকারেন্ট অপারেশন জড়িত থাকে, তখন আমরা দ্রুতই অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের কিছু নতুন চ্যালেঞ্জের মুখোমুখি হই, যেখানে অপারেশনগুলো যে ক্রমে শুরু হয়েছিল সেই ক্রমে শেষ নাও হতে পারে। এই অধ্যায়টি চ্যাপ্টার ১৬-তে প্যারালালিসম এবং কনকারেন্সির জন্য থ্রেডের ব্যবহারের উপর ভিত্তি করে তৈরি হয়েছে এবং অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের জন্য একটি বিকল্প পদ্ধতির সাথে পরিচয় করিয়ে দেবে: রাস্টের Futures, Streams, তাদের সমর্থনকারী async
এবং await
সিনট্যাক্স, এবং অ্যাসিঙ্ক্রোনাস অপারেশনগুলো পরিচালনা ও সমন্বয় করার টুলস।
আসুন একটি উদাহরণ বিবেচনা করা যাক। ধরুন আপনি একটি পারিবারিক অনুষ্ঠানের ভিডিও এক্সপোর্ট করছেন, এই কাজটি শেষ হতে মিনিট থেকে ঘণ্টা পর্যন্ত সময় লাগতে পারে। ভিডিও এক্সপোর্টটি তার সাধ্যমতো CPU এবং GPU পাওয়ার ব্যবহার করবে। যদি আপনার কেবল একটি CPU কোর থাকত এবং আপনার অপারেটিং সিস্টেম এক্সপোর্টটি শেষ না হওয়া পর্যন্ত থামিয়ে না রাখত—অর্থাৎ, যদি এটি এক্সপোর্টটি সিঙ্ক্রোনাসভাবে (synchronously) চালাত—তাহলে ঐ টাস্ক চলাকালীন আপনি আপনার কম্পিউটারে অন্য কিছুই করতে পারতেন না। এটি একটি বেশ হতাশাজনক অভিজ্ঞতা হতো। ভাগ্যক্রমে, আপনার কম্পিউটারের অপারেটিং সিস্টেম এক্সপোর্টটিকে প্রায়শই অদৃশ্যভাবে বাধাগ্রস্ত করতে পারে যাতে আপনি একই সাথে অন্যান্য কাজ করতে পারেন।
এখন ধরুন আপনি অন্য কারো শেয়ার করা একটি ভিডিও ডাউনলোড করছেন, যা শেষ হতেও বেশ সময় লাগতে পারে কিন্তু এটি ততটা CPU সময় নেয় না। এক্ষেত্রে, নেটওয়ার্ক থেকে ডেটা আসার জন্য CPU-কে অপেক্ষা করতে হয়। ডেটা আসা শুরু হলে আপনি ডেটা পড়া শুরু করতে পারলেও, সব ডেটা আসতে কিছুটা সময় লাগতে পারে। এমনকি সব ডেটা উপস্থিত থাকলেও, যদি ভিডিওটি বেশ বড় হয়, তবে এটি লোড করতে অন্তত এক বা দুই সেকেন্ড সময় লাগতে পারে। এটি হয়তো খুব বেশি সময় মনে নাও হতে পারে, কিন্তু একটি আধুনিক প্রসেসরের জন্য এটি অনেক দীর্ঘ সময়, যা প্রতি সেকেন্ডে বিলিয়ন অপারেশন করতে পারে। আবারও, আপনার অপারেটিং সিস্টেম আপনার প্রোগ্রামকে অদৃশ্যভাবে বাধাগ্রস্ত করবে যাতে নেটওয়ার্ক কল শেষ হওয়ার জন্য অপেক্ষা করার সময় CPU অন্য কাজ করতে পারে।
ভিডিও এক্সপোর্ট হলো একটি CPU-বাউন্ড বা কম্পিউট-বাউন্ড অপারেশনের উদাহরণ। এটি কম্পিউটারের CPU বা GPU-এর ডেটা প্রসেসিং স্পিডের উপর সীমাবদ্ধ, এবং সেই স্পিডের কতটা অংশ এটি অপারেশনে উৎসর্গ করতে পারে তার উপর নির্ভরশীল। ভিডিও ডাউনলোড হলো একটি IO-বাউন্ড অপারেশনের উদাহরণ, কারণ এটি কম্পিউটারের ইনপুট এবং আউটপুট এর গতির দ্বারা সীমাবদ্ধ; এটি কেবল তত দ্রুত চলতে পারে যত দ্রুত নেটওয়ার্কের মাধ্যমে ডেটা পাঠানো যায়।
এই উভয় উদাহরণে, অপারেটিং সিস্টেমের অদৃশ্য ইন্টারাপ্টগুলো এক ধরনের কনকারেন্সি প্রদান করে। তবে এই কনকারেন্সি কেবল পুরো প্রোগ্রামের স্তরে ঘটে: অপারেটিং সিস্টেম একটি প্রোগ্রামকে বাধাগ্রস্ত করে অন্য প্রোগ্রামগুলোকে কাজ করার সুযোগ দেয়। অনেক ক্ষেত্রে, যেহেতু আমরা আমাদের প্রোগ্রামগুলোকে অপারেটিং সিস্টেমের চেয়ে অনেক বেশি বিস্তারিত স্তরে বুঝি, তাই আমরা কনকারেন্সির এমন সুযোগগুলো চিহ্নিত করতে পারি যা অপারেটিং সিস্টেম দেখতে পায় না।
উদাহরণস্বরূপ, যদি আমরা ফাইল ডাউনলোড পরিচালনা করার জন্য একটি টুল তৈরি করি, আমাদের প্রোগ্রামটি এমনভাবে লেখা উচিত যাতে একটি ডাউনলোড শুরু করলে UI লক হয়ে না যায়, এবং ব্যবহারকারীরা একই সাথে একাধিক ডাউনলোড শুরু করতে পারেন। নেটওয়ার্কের সাথে ইন্টারঅ্যাক্ট করার জন্য অনেক অপারেটিং সিস্টেম এপিআই (API) ব্লকিং (blocking) হয়ে থাকে; অর্থাৎ, তারা যে ডেটা প্রসেস করছে তা সম্পূর্ণ প্রস্তুত না হওয়া পর্যন্ত প্রোগ্রামের অগ্রগতি আটকে রাখে।
দ্রষ্টব্য: আপনি যদি ভেবে দেখেন, তবে বেশিরভাগ ফাংশন কল এভাবেই কাজ করে। তবে, ব্লকিং শব্দটি সাধারণত সেই ফাংশন কলগুলোর জন্য সংরক্ষিত যা ফাইল, নেটওয়ার্ক বা কম্পিউটারের অন্যান্য রিসোর্সের সাথে ইন্টারঅ্যাক্ট করে, কারণ এই ক্ষেত্রগুলিতে একটি স্বতন্ত্র প্রোগ্রাম অপারেশনটি নন-ব্লকিং (non-blocking) হলে উপকৃত হবে।
আমরা প্রতিটি ফাইল ডাউনলোড করার জন্য একটি ডেডিকেটেড থ্রেড তৈরি করে আমাদের প্রধান থ্রেডকে ব্লক করা এড়াতে পারতাম। তবে, সেই থ্রেডগুলোর ওভারহেড অবশেষে একটি সমস্যা হয়ে দাঁড়াবে। বরং ভালো হতো যদি কলটি প্রথম স্থানেই ব্লক না করত। আরও ভালো হতো যদি আমরা ব্লকিং কোডের মতো একই সরাসরি স্টাইলে লিখতে পারতাম, যেমনটা নিচে দেখানো হয়েছে:
let data = fetch_data_from(url).await;
println!("{data}");
রাস্টের async (যা asynchronous এর সংক্ষিপ্ত রূপ) অ্যাবস্ট্র্যাকশন ঠিক এটাই আমাদের দেয়। এই অধ্যায়ে, আপনি async সম্পর্কে সবকিছু শিখবেন যখন আমরা নিম্নলিখিত বিষয়গুলি নিয়ে আলোচনা করব:
- রাস্টের
async
এবংawait
সিনট্যাক্স কীভাবে ব্যবহার করবেন - চ্যাপ্টার ১৬-তে আমরা যে চ্যালেঞ্জগুলো দেখেছিলাম তার কিছু সমাধান করতে async মডেল কীভাবে ব্যবহার করবেন
- কীভাবে মাল্টিথ্রেডিং এবং async পরিপূরক সমাধান প্রদান করে, যা আপনি অনেক ক্ষেত্রে একত্রিত করতে পারেন
তবে async বাস্তবে কীভাবে কাজ করে তা দেখার আগে, আমাদের প্যারালালিসম এবং কনকারেন্সির মধ্যে পার্থক্য নিয়ে আলোচনা করার জন্য একটি সংক্ষিপ্ত পথ পাড়ি দিতে হবে।
প্যারালালিসম এবং কনকারেন্সি (Parallelism and Concurrency)
এখন পর্যন্ত আমরা প্যারালালিসম এবং কনকারেন্সিকে মূলত একই জিনিস হিসেবে গণ্য করেছি। এখন আমাদের এদের মধ্যে আরও স্পষ্টভাবে পার্থক্য করতে হবে, কারণ আমরা কাজ শুরু করার সাথে সাথে এই পার্থক্যগুলো প্রকাশ পাবে।
একটি সফটওয়্যার প্রকল্পে একটি দল কীভাবে কাজ ভাগ করে নিতে পারে তার বিভিন্ন উপায় বিবেচনা করুন। আপনি একজন সদস্যকে একাধিক কাজ দিতে পারেন, প্রতিটি সদস্যকে একটি করে কাজ দিতে পারেন, অথবা দুটি পদ্ধতির মিশ্রণ ব্যবহার করতে পারেন।
যখন একজন ব্যক্তি একাধিক ভিন্ন কাজ সম্পন্ন করার আগে সেগুলোর উপর কাজ করে, তখন এটি হলো কনকারেন্সি (concurrency)। হয়তো আপনার কম্পিউটারে দুটি ভিন্ন প্রজেক্ট চেক আউট করা আছে, এবং যখন আপনি একটি প্রকল্পে বিরক্ত বা আটকে যান, তখন আপনি অন্যটিতে চলে যান। আপনি কেবল একজন ব্যক্তি, তাই আপনি একই সময়ে উভয় কাজে অগ্রগতি করতে পারবেন না, কিন্তু আপনি মাল্টি-টাস্ক করতে পারেন, একটি থেকে অন্যটিতে সুইচ করে একবারে একটিতে অগ্রগতি করতে পারেন (চিত্র ১৭-১ দেখুন)।
যখন দলটি প্রতিটি সদস্যকে একটি করে কাজ দিয়ে এবং একা একা কাজ করতে বলে কাজের একটি গ্রুপ ভাগ করে নেয়, তখন এটি হলো প্যারালালিসম (parallelism)। দলের প্রত্যেক ব্যক্তি একই সময়ে অগ্রগতি করতে পারে (চিত্র ১৭-২ দেখুন)।
এই উভয় ওয়ার্কফ্লোতে, আপনাকে বিভিন্ন কাজের মধ্যে সমন্বয় করতে হতে পারে। হয়তো আপনি ভেবেছিলেন যে একজন ব্যক্তিকে দেওয়া কাজটি অন্যদের কাজ থেকে সম্পূর্ণ স্বাধীন, কিন্তু আসলে এটি দলের অন্য একজন ব্যক্তির কাজ শেষ করার উপর নির্ভরশীল। কিছু কাজ প্যারালালি করা যেত, কিন্তু কিছু কাজ আসলে সিরিয়াল (serial) ছিল: এটি কেবল একটি সিরিজের মতো, একের পর এক টাস্ক হিসেবে হতে পারত, যেমনটি চিত্র ১৭-৩ এ দেখানো হয়েছে।
একইভাবে, আপনি হয়তো বুঝতে পারেন যে আপনার নিজের একটি কাজ আপনার অন্য একটি কাজের উপর নির্ভরশীল। এখন আপনার কনকারেন্ট কাজও সিরিয়াল হয়ে গেছে।
প্যারালালিসম এবং কনকারেন্সি একে অপরের সাথে ছেদ করতে পারে। যদি আপনি জানতে পারেন যে একজন সহকর্মী আপনার একটি কাজ শেষ না করা পর্যন্ত আটকে আছেন, তাহলে আপনি সম্ভবত আপনার সহকর্মীকে "আনব্লক" করার জন্য সেই কাজের উপর আপনার সমস্ত প্রচেষ্টা কেন্দ্রীভূত করবেন। আপনি এবং আপনার সহকর্মী আর প্যারালালি কাজ করতে পারছেন না, এবং আপনি আর আপনার নিজের কাজগুলিতে কনকারেন্টলি কাজ করতে পারছেন না।
একই মৌলিক গতিবিদ্যা সফটওয়্যার এবং হার্ডওয়্যারের ক্ষেত্রেও প্রযোজ্য। একটি একক CPU কোর সহ একটি মেশিনে, CPU একবারে কেবল একটি অপারেশন করতে পারে, কিন্তু এটি এখনও কনকারেন্টলি কাজ করতে পারে। থ্রেড, প্রসেস এবং async-এর মতো টুল ব্যবহার করে, কম্পিউটার একটি কার্যকলাপকে থামিয়ে অন্যগুলিতে সুইচ করতে পারে এবং অবশেষে সেই প্রথম কার্যকলাপে আবার ফিরে আসতে পারে। একাধিক CPU কোর সহ একটি মেশিনে, এটি প্যারালালিও কাজ করতে পারে। একটি কোর একটি কাজ করতে পারে যখন অন্য একটি কোর একটি সম্পূর্ণ সম্পর্কহীন কাজ করে, এবং সেই অপারেশনগুলি আসলে একই সময়ে ঘটে।
রাস্টে async নিয়ে কাজ করার সময়, আমরা সবসময় কনকারেন্সি নিয়ে কাজ করি। হার্ডওয়্যার, অপারেটিং সিস্টেম এবং আমরা যে async রানটাইম ব্যবহার করছি তার উপর নির্ভর করে (async রানটাইম সম্পর্কে শীঘ্রই আরও আলোচনা করা হবে), সেই কনকারেন্সি পর্দার আড়ালে প্যারালালিসমও ব্যবহার করতে পারে।
এখন, চলুন রাস্টের অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং আসলে কীভাবে কাজ করে তা নিয়ে আলোচনা করা যাক।