JavaScript থেকে Python-এ চলে আসা অনেকটা একই রাস্তার সাইন কিন্তু ভিন্ন ট্রাফিক আইনের কোনো শহরে চলে আসার মতো। সিনট্যাক্স দেখতে বন্ধুত্বপূর্ণ এবং পরিচিত মনে হয়। আপনি গ্রামারে async এবং await দেখতে পান, তাই আপনি ধরে নেন যে এর মেন্টাল মডেলটিও একইভাবে কাজ করবে। কিন্তু তা হয় না। JavaScript-এর একটি অভ্যাসগত প্যাটার্ন আপনার Python পারফরম্যান্সকে কোনো ক্র্যাশ ছাড়াই, কোনো এরর লগ ছাড়াই এবং কোড রিভিউতে ধরা না পড়েই নীরবে কমিয়ে দেয়।

JavaScript আপনাকে কীভাবে শুরু করে ভুলে থাকতে শেখায়

JavaScript-এ, একটি async ফাংশন কল হলো 'eager' (তৎক্ষণাৎ কার্যকর)। আপনি যখনই এটি কল করেন, ইঞ্জিন একটি Promise তৈরি করে এবং কাজ সাথে সাথে শুরু হয়ে যায়। ইভেন্ট লুপ ইতিমধ্যেই দৌড় শুরু করে দিয়েছে। এই কারণেই JavaScript ডেভেলপাররা স্বাভাবিকভাবেই এভাবে কোড লেখেন:

const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;

প্রথম await-এ পৌঁছানোর আগেই উভয় নেটওয়ার্ক রিকোয়েস্টই চলতে শুরু করে। প্রথম await বর্তমান ফাংশনটিকে fetchUser রেজলভ না হওয়া পর্যন্ত স্থগিত রাখে, কিন্তু fetchOrders আগের লাইন থেকেই ব্যাকগ্রাউন্ডে কাজ চালিয়ে যাচ্ছে। আপনি যখন orders ভেরিয়েবলটি ব্যবহার করতে যান, ততক্ষণে দ্বিতীয় রিকোয়েস্টটি হয়তো শেষ হয়ে গেছে। JavaScript-এ এই প্যাটার্নটি এতই স্বাভাবিক যে অনেক ডেভেলপার এটিকে কনকারেন্সি ট্রিক হিসেবেও ভাবেন না। এটি কেবল async-এর কাজের ধরন।

Python-এর বিস্ময়: একটি কোল্ড কোরুটিন (Cold Coroutine)

Python ভিন্ন একটি নিয়ম মেনে চলে। Python-এ যখন আপনি একটি async def ফাংশন কল করেন, তখন আপনি কোনো কাজ শুরু করেন না। আপনি একটি coroutine অবজেক্ট পান। এটিকে কাগজে লেখা একটি রেসিপির মতো ভাবুন। উপকরণগুলো তালিকাভুক্ত আছে, ধাপগুলো স্পষ্ট, কিন্তু ওভেনে কিছুই নেই। যতক্ষণ না কোনো কিছু স্পষ্টভাবে সেই কোরুটিনটিকে ইভেন্ট লুপের মাধ্যমে চালনা করছে, ততক্ষণ এটি নিষ্ক্রিয় থাকে।

এখানেই ফাঁদ। একজন JavaScript ইঞ্জিনিয়ার যার একজন ইউজার এবং তার অর্ডার প্রয়োজন, তিনি Python-এ এটি লিখতে পারেন:

user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro

এটি দেখতে কনকারেন্ট মনে হয়। কনকারেন্ট মনে হয়। কিন্তু এটি সম্পূর্ণ সিকোয়েন্সিয়াল (একটির পর একটি)।

প্রথম লাইনটি একটি সুপ্ত (dormant) কোরুটিনকে user_coro-তে অ্যাসাইন করে। দ্বিতীয় লাইনটি আরেকটি সুপ্ত কোরুটিনকে orders_coro-তে অ্যাসাইন করে। যখন এক্সিকিউশন await user_coro-তে পৌঁছায়, Python অবশেষে প্রথম টাস্কটি শুরু করে এবং সেটি সম্পন্ন হওয়া পর্যন্ত চালায়। fetch_user শেষ হওয়ার পরেই ইন্টারপ্রেটার await orders_coro-তে পৌঁছায় এবং দ্বিতীয় টাস্কটি শুরু করে। আপনার মোট এক্সিকিউশন টাইম হলো উভয় I/O অপারেশনের যোগফল, দীর্ঘতমটির নয়। আপনি এগুলো প্যারালালে চালাননি। আপনি এগুলো অতিরিক্ত ধাপসহ একের পর এক চালিয়েছেন।

কেন এই বাগটি অদৃশ্য থাকে

এটি এমন এক ধরনের পারফরম্যান্স রিগ্রেশন যা মাসের পর মাস টিকে থাকে। কোডটি সঠিক Python কোড। এটি টাইপ চেকার পাস করে। এটি সঠিক ফলাফল প্রদান করে। এটি কেবল অর্ধেক গতিতে বা তার চেয়েও ধীরগতিতে চলে। যেহেতু কোনো স্ট্যাক ট্রেস বা ওয়ার্নিং থাকে না, তাই ইঞ্জিনিয়ারিং টিমগুলো প্রায়ই অন্য সব জায়গায় খোঁজাখুঁজি করে। তারা Redis ক্যাশ যোগ করে, ডাটাবেস টায়ার আপগ্রেড করে অথবা হোস্টিং রিজিয়ন পরিবর্তন করে। আসল অপরাধী হলো await আসলে কী করে, সেই বিষয়ে প্রত্যাশার একটি সূক্ষ্ম অমিল।

Python-কে প্রকৃতপক্ষে কনকারেন্টলি চালানোর তিনটি উপায়

এটি ঠিক করতে, আপনাকে Python-এর ইভেন্ট লুপকে অবিলম্বে কাজ শিডিউল করার নির্দেশ দিতে হবে। আপনার একটি র (raw) কোরুটিনের চেয়ে আরও সক্রিয় কিছুর প্রয়োজন। আপনার একটি Task প্রয়োজন।

1. asyncio.create_task

JavaScript প্যাটার্নের সবচেয়ে সরাসরি অনুবাদ হলো আপনার কোরুটিনটিকে একটি Task-এ মুড়িয়ে (wrap) দেওয়া। একটি Task তৈরি করার সাথে সাথেই সেটি ইভেন্ট লুপে শিডিউল হয়ে যায়। এটি চলমান একটি JavaScript Promise-এর সবচেয়ে কাছাকাছি Python সমতুল্য।

user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))

user = await user_task
orders = await_orders_task

এখন প্রথম await-এর আগেই fetch_user এবং fetch_orders উভয়ই চলতে শুরু করে। যখন আপনি await user_task-এ পৌঁছান, আপনি কেবল সেই নির্দিষ্ট Taskটি সম্পন্ন না হওয়া পর্যন্ত থামেন, কিন্তু অন্য Taskটি চলতে থাকে। যদি fetch_orders আগে শেষ হয়, তবে এর ফলাফলটি orders_task-এর ভেতরে আপনার কাছে চাওয়া পর্যন্ত অপেক্ষা করে।

তবে সাবধান। আপনি যদি একটি Task তৈরি করেন এবং সেটি কখনোই await না করেন, তবে Python একটি 'destroyed pending task' সংক্রান্ত এরর দেবে। আপনাকে অবশ্যই আপনার ফলাফল সংগ্রহ করতে হবে।

2. asyncio.gather

যদি আপনার কাছে এমন বেশ কিছু কোরুটিন থাকে যেগুলোকে পরবর্তী ধাপে যাওয়ার আগে শেষ করতে হবে, তবে asyncio.gather আপনার হয়ে সেই কাজগুলো করে দেয়। এটি অভ্যন্তরীণভাবে প্রতিটি কোরুটিনকে একটি Task হিসেবে শিডিউল করে এবং তাদের একসাথে await করে।

user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))

এটি সংক্ষিপ্ত এবং পঠনযোগ্য। যখন অপারেশনগুলো স্বাধীন হয় এবং আপনি একটি মাত্র লাইনে "এগুলো সব চালাও, তারপর আমাকে প্রতিটি ফলাফল দাও" এমন কিছু প্রকাশ করতে চান, তখন এটি দারুণ কাজ করে। এটি রিটার্ন করা লিস্ট বা টাপল-এ আর্গুমেন্টের ক্রম বজায় রাখে, এমনকি যদি অন্তর্নিহিত টাস্কগুলো ভিন্ন ক্রমে সম্পন্ন হয় তবুও।

3. asyncio.TaskGroup

Python 3.11-এ TaskGroup প্রবর্তন করা হয়েছে, যা স্ট্যান্ডার্ড লাইব্রেরিতে structured concurrency নিয়ে এসেছে। হাতে কলমে টাস্ক তৈরি করার পরিবর্তে, আপনি একটি context manager ব্যবহার করতে পারেন যা নিশ্চিত করে যে প্রতিটি স্পন করা (spawned) টাস্ক সঠিকভাবে শেষ হয়েছে। যদি একটি টাস্ক কোনো exception রেইজ করে, তবে অন্যগুলো স্বয়ংক্রিয়ভাবে বাতিল (cancel) হয়ে যায়।

async with asyncio.TaskGroup() as tg:
    user_task = tg.create_task(fetch_user(id))
    orders_task = tg.create_task(fetch_orders(id))

user = user_task.result()
orders = orders_task.result()

জটিল ওয়ার্কফ্লোর জন্য এই প্যাটার্নটি চমৎকার। এটি একটি Task-কে অনাথ (orphan) করার ঝুঁকি দূর করে এবং সম্পর্কিত অপারেশনগুলোর লাইফসাইকেলকে একটি যৌক্তিক কাঠামোর (logical umbrella) অধীনে গ্রুপ করে। আপনার কোডবেস যদি Python 3.11 বা তার পরবর্তী ভার্সনে চলে, তবে fan-out concurrency-র জন্য এটিই প্রায়শই সবচেয়ে পরিচ্ছন্ন আর্কিটেকচার।

মেন্টাল মডেল: await মানে হলো "এটি এখনই চালান"

মূল শিক্ষাটি ভাষাগত। JavaScript-এ, আপনি await-কে "meanwhile" বা "ইতিমধ্যে" হিসেবে পড়তে পারেন। আপনি কাজ শুরু করেন, অন্যান্য কাজ করেন এবং শুধুমাত্র যখন ভ্যালুটির প্রয়োজন হয় তখনই থামেন। Python-এ, await মানে হলো "এই coroutine-টিকে তার পরবর্তী সাসপেনশন পয়েন্ট বা সমাপ্তি পর্যন্ত চালনা করুন।" যদি coroutine-টি এখনও শিডিউল করা না হয়ে থাকে, তবে await-ই সেটি শিডিউল করে। এই কারণেই আপনি দুটি র (raw) coroutine শুরু করে পরে সেগুলোকে await করতে পারেন না। আপনি ইতিমধ্যে ইভেন্ট লুপকে করার মতো কিছু দেননি।

Python coroutine-গুলোকে generator function-এর মতো ভাবুন। জেনারেটর কল করলেই সেটি ইটারেট (iterate) হয় না। আপনাকে এর ওপর লুপ চালাতে হবে, next() কল করতে হবে, অথবা কোনো কনজিউমার-এর কাছে পাঠাতে হবে। Async একইভাবে কাজ করে। asyncio.create_task হলো সেই কনজিউমার যা বলে "এটি এখনই ইভেন্ট লুপে রাখুন।" পরবর্তী await কেবল সমাপ্তির সংকেতের জন্য অপেক্ষা করে।

একটি কার্যকর অভ্যাস যা সাহায্য করতে পারে: যখনই আপনি কোনো await ছাড়াই একটি async ফাংশন কলকে কোনো ভেরিয়েবলে অ্যাসাইন করবেন, নিজেকে জিজ্ঞাসা করুন যে আপনি সেটি শিডিউল করেছেন কি না। যদি ডানদিকের অংশটি create_task, gather, অথবা TaskGroup-এর মধ্যে না থাকে, তবে এটি চলছে না। এটি কেবল কাউন্টারে রাখা একটি রেসিপির মতো।

সারসংক্ষেপ

Python-এর async runtime শক্তিশালী, কিন্তু এটি স্পষ্ট উদ্দেশ্যের (explicit intent) দাবি রাখে। আপনি কেবল একটি ফাংশন কল করেছেন বলেই ভাষাটি ব্যাকগ্রাউন্ডে কাজ শুরু করে দেয় না। আপনি যদি JavaScript থেকে আসেন, তবে যেখানেই আপনি একটি coroutine-কে ভেরিয়েবলে সংরক্ষণ করছেন এবং পরে সেটি await করছেন, সেই প্রতিটি জায়গা অডিট (audit) করুন। যদি না আপনি প্রথমে এটিকে একটি Task-এ উন্নীত (promote) করেন, তবে আপনি আসলে async-এর পোশাকে সিকোয়েন্সিয়াল (sequential) কোড লিখেছেন। একটি Task-এর মাধ্যমে কাজ শুরু করুন, তারপর ফলাফলের জন্য অপেক্ষা করুন। এভাবেই আপনি Python async-কে একটি নীরব বাধা (silent bottleneck) থেকে একটি প্রকৃত concurrency টুলে রূপান্তরিত করতে পারেন।