JavaScript سے Python پر منتقل ہونا ایسا ہی ہے جیسے کسی ایسے شہر میں منتقل ہونا جہاں سڑکوں کے نشانات تو وہی ہوں لیکن ٹریفک کے قوانین مختلف ہوں۔ اس کا سنٹیکس (syntax) دوستانہ اور مانوس لگتا ہے۔ آپ گرامر میں async اور await کو بالکل ویسے ہی دیکھتے ہیں، اس لیے آپ فرض کر لیتے ہیں کہ ذہنی ماڈل (mental model) آسانی سے منتقل ہو جائے گا۔ ایسا نہیں ہوتا۔ JavaScript کا ایک عادی پیٹرن آپ کی Python پرفارمنس کو خاموشی سے تباہ کر دیتا ہے، بغیر کریش ہوئے، بغیر کسی ایرر (error) کے، اور بغیر کسی فوری کوڈ ریویو میں نظر آئے ۔
JavaScript آپ کو کیسے "شروع کر کے بھول جانے" کا طریقہ سکھاتا ہے
JavaScript میں، ایک async فنکشن کال "eager" ہوتی ہے۔ جیسے ہی آپ اسے کال کرتے ہیں، انجن ایک Promise بنا دیتا ہے اور کام فوراً شروع ہو جاتا ہے۔ ایونٹ لوپ (event loop) پہلے ہی کام پر لگ چکا ہوتا ہے۔ یہی وجہ ہے کہ JavaScript ڈویلپرز قدرتی طور پر اس طرح کوڈ لکھتے ہیں:
const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;
کسی بھی await تک پہنچنے سے پہلے دونوں نیٹ ورک ریکویسٹ (network requests) چل رہی ہوتی ہیں۔ پہلا await موجودہ فنکشن کو تب تک روک دیتا ہے جب تک fetchUser مکمل نہ ہو جائے، لیکن fetchOrders پچھلی لائن سے ہی پس منظر (background) میں کام کر رہا ہوتا ہے۔ جب تک آپ کو orders ویری ایبل کی ضرورت ہوتی ہے، دوسری ریکویسٹ شاید پہلے ہی مکمل ہو چکی ہو۔ JavaScript میں یہ پیٹرن اتنا قدرتی لگتا ہے کہ بہت سے ڈویلپرز اسے کنکرنسی (concurrency) کی تکنیک کے طور پر بھی نہیں سوچتے۔ یہ بس async کے کام کرنے کا طریقہ ہے۔
Python کا سرپرائز: ایک ٹھنڈا Coroutine
Python ایک مختلف معاہدہ (contract) استعمال کرتا ہے۔ جب آپ Python میں async def فنکشن کو کال کرتے ہیں، تو آپ کوئی کام شروع نہیں کرتے۔ آپ کو ایک coroutine آبجیکٹ ملتا ہے۔ اسے کاغذ پر لکھی ہوئی ایک ترکیب (recipe) کے طور پر سمجھیں۔ اجزاء درج ہیں، مراحل واضح ہیں، لیکن اوون میں کچھ نہیں ہے۔ جب تک کوئی چیز اس coroutine کو ایونٹ لوپ کے ذریعے واضح طور پر آگے نہیں بڑھاتی، یہ غیر فعال (inert) رہتا ہے۔
یہاں جال ہے۔ ایک JavaScript انجینئر جسے صارف اور اس کے آرڈرز کی ضرورت ہو، وہ Python میں یہ لکھ سکتا ہے:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
یہ کنکرنٹ (concurrent) لگتا ہے۔ یہ کنکرنٹ محسوس ہوتا ہے۔ لیکن یہ مکمل طور پر سیکوئنشل (sequential) ہے۔
پہلی لائن user_coro کو ایک غیر فعال coroutine تفویض کرتی ہے۔ دوسری لائن orders_coro کو ایک اور غیر فعال coroutine تفویض کرتی ہے۔ جب ایگزیکیوشن (execution) await user_coro پر پہنچتی ہے، تو Python آخر کار پہلا ٹاسک شروع کرتا ہے اور اسے مکمل کرتا ہے۔ صرف fetch_user ختم ہونے کے بعد ہی انٹرپریٹر await orders_coro تک پہنچتا ہے اور دوسرا ٹاسک شروع کرتا ہے۔ آپ کا کل ایگزیکیوشن ٹائم دونوں I/O آپریشنز کا مجموعہ ہوتا ہے، نہ کہ سب سے طویل آپریشن کا۔ آپ نے انہیں متوازی (parallel) نہیں چلایا۔ آپ نے انہیں اضافی مراحل کے ساتھ ایک کے بعد ایک چلایا۔
یہ بگ (bug) نظر کیوں نہیں آتا
یہ اس قسم کی پرفارمنس ریگریشن (performance regression) ہے جو مہینوں تک برقرار رہتی ہے۔ کوڈ درست Python ہے۔ یہ ٹائپ چیکرز (type checkers) کو پاس کر لیتا ہے۔ یہ درست نتائج واپس کرتا ہے۔ یہ بس آدھی رفتار سے، یا اس سے بھی بدتر، چلتا ہے۔ چونکہ کوئی اسٹیک ٹریس (stack trace) اور کوئی وارننگ نہیں ہوتی، اس لیے انجینئرنگ ٹیمیں اکثر پہلے ہر دوسری جگہ تلاش کرتی ہیں۔ وہ Redis کیشز (caches) شامل کرتے ہیں، ڈیٹا بیس ٹائرز (database tiers) کو اپ گریڈ کرتے ہیں، یا ہوسٹنگ ریجن (hosting regions) تبدیل کرتے ہیں۔ اصل مجرم await کے اصل کام کے بارے میں توقعات میں ایک باریک فرق ہے۔
Python کو حقیقت میں چیزوں کو کنکرنٹلی چلانے کے تین طریقے
اسے ٹھیک کرنے کے لیے، آپ کو Python کے ایونٹ لوپ کو کام کو فوری طور پر شیڈول کرنے کا کہنا ہوگا۔ آپ کو ایک سادہ coroutine سے زیادہ فعال چیز کی ضرورت ہے۔ آپ کو ایک Task کی ضرورت ہے۔
1. asyncio.create_task
JavaScript پیٹرن کا سب سے براہ راست ترجمہ اپنے coroutine کو ایک 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
اگر آپ کے پاس کئی coroutines ہیں جنہیں آگے بڑھنے سے پہلے مکمل ہونا ضروری ہے، تو asyncio.gather آپ کے لیے بوائلر پلیٹ (boilerplate) کوڈ سنبھال لیتا ہے۔ یہ اندرونی طور پر ہر coroutine کو ایک Task کے طور پر شیڈول کرتا ہے اور انہیں ایک ساتھ await کرتا ہے۔
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
یہ مختصر اور پڑھنے میں آسان ہے۔ یہ اس وقت بہترین کام کرتا ہے جب آپریشنز آزاد ہوں اور آپ ایک ایسی لائن چاہتے ہوں جو کہے "ان سب کو چلائیں، پھر مجھے ہر نتیجہ دیں"۔ یہ واپس آنے والی لسٹ یا ٹپل (tuple) میں آرگومنٹ (arguments) کی ترتیب کو بھی برقرار رکھتا ہے، چاہے بنیادی ٹاسک مختلف ترتیب میں مکمل ہوں۔
3. asyncio.TaskGroup
Python 3.11 نے TaskGroup متعارف کرایا ہے، جو اسٹینڈرڈ لائبریری میں structured concurrency لاتا ہے۔ ٹاسکوں کو خود سے بنانے کے بجائے، آپ ایک context manager استعمال کرتے ہیں جو اس بات کو یقینی بناتا ہے کہ ہر تخلیق کردہ (spawned) ٹاسک صحیح طریقے سے مکمل ہو۔ اگر ایک ٹاسک کوئی exception پیدا کرتا ہے، تو باقی تمام ٹاسک خود بخود کینسل ہو جاتے ہیں۔
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()
یہ پیٹرن پیچیدہ workflows کے لیے بہترین ہے۔ یہ کسی ٹاسک کے یتیم (orphan) ہونے کے خطرے کو ختم کرتا ہے، اور متعلقہ آپریشنز کی لائف سائیکل کو ایک منطقی چھتری کے نیچے گروپ کرتا ہے۔ اگر آپ کا کوڈ بیس Python 3.11 یا اس سے نئے ورژن پر چلتا ہے، تو fan-out concurrency کے لیے یہ اکثر سب سے بہترین اور صاف ستھرا آرکیٹیکچر ہوتا ہے۔
ذہنی ماڈل: await کا مطلب ہے "اسے ابھی چلائیں"
بنیادی سبق لسانی ہے۔ JavaScript میں، آپ await کو "اس دوران" (meanwhile) کے طور پر پڑھ سکتے ہیں۔ آپ کام شروع کرتے ہیں، دوسرے کام کرتے ہیں، اور صرف اس وقت رکتے ہیں جب آپ کو اس ویلیو کی ضرورت ہو۔ Python میں، await کا مطلب ہے "اس coroutine کو اس کے اگلے suspension point یا تکمیل تک لے کر جائیں۔" اگر coroutine ابھی تک شیڈول نہیں کی گئی ہے، تو await ہی اسے شیڈول کرتا ہے۔ یہی وجہ ہے کہ آپ دو خام (raw) coroutines شروع نہیں کر سکتے اور پھر بعد میں انہیں await نہیں کر سکتے۔ آپ نے اس دوران event loop کو کرنے کے لیے کچھ نہیں دیا۔
Python coroutines کو generator functions کی طرح سمجھیں۔ جنریٹر کو کال کرنے سے اس کی iteration نہیں ہوتی۔ آپ کو اس پر لوپ چلانے، next() کال کرنے، یا اسے کسی consumer کو دینے کی ضرورت ہوتی ہے۔ Async بھی اسی طرح کام کرتا ہے۔ asyncio.create_task وہ consumer ہے جو کہتا ہے "اسے ابھی اسی وقت event loop پر ڈال دیں۔" اس کے بعد والا await محض تکمیل کے سگنل کا انتظار کرتا ہے۔
ایک عملی عادت جو مددگار ثابت ہوتی ہے: جب بھی آپ کسی async فنکشن کال کو await کے بغیر کسی ویری ایبل میں اسائن کرتے ہیں، تو خود سے پوچھیں کہ کیا آپ نے اسے شیڈول کیا ہے؟ اگر دائیں جانب (right-hand side) create_task، gather یا TaskGroup میں لپٹا ہوا نہیں ہے، تو یہ چل نہیں رہا ہے۔ یہ محض کاؤنٹر پر پڑی ہوئی ایک ترکیب (recipe) کی طرح ہے۔
خلاصہ
Python کا async runtime طاقتور ہے، لیکن یہ واضح ارادے (explicit intent) کا تقاضا کرتا ہے۔ زبان صرف اس لیے بیک گراؤنڈ کام شروع نہیں کرتی کیونکہ آپ نے کسی فنکشن کو کال کیا ہے۔ اگر آپ JavaScript سے آ رہے ہیں، تو ہر اس جگہ کا جائزہ لیں جہاں آپ ایک coroutine کو ویری ایبل میں محفوظ کرتے ہیں اور بعد میں اسے await کرتے ہیں۔ جب تک آپ نے اسے پہلے Task میں تبدیل نہیں کیا، آپ نے محض async کے لباس میں لپٹا ہوا sequential کوڈ لکھا ہے۔ کام کو ایک Task کے ساتھ شروع کریں، پھر نتائج کا انتظار کریں۔ اسی طرح آپ Python async کو ایک خاموش رکاوٹ (bottleneck) سے ایک حقیقی concurrency ٹول میں بدل سکتے ہیں۔
