JavaScript నుండి Python కి మారడం అనేది ఒకే రకమైన వీధి గుర్తులు ఉన్నప్పటికీ, ట్రాఫిక్ నిబంధనలు వేరుగా ఉన్న నగరానికి మారినట్లు అనిపిస్తుంది. దీని సింటాక్స్ (syntax) స్నేహపూర్వకంగా మరియు పరిచయంగా అనిపిస్తుంది. గ్రామర్లో async మరియు await నేరుగా ఉండటం వల్ల, మీ మెంటల్ మోడల్ (mental model) కూడా అలాగే పనిచేస్తుందని మీరు అనుకుంటారు. కానీ అది నిజం కాదు. JavaScript నుండి అలవాటైన ఒక పద్ధతి, ఎటువంటి ఎర్రర్ (error) చూపించకుండా లేదా కోడ్ రివ్యూలో దొరకకుండానే, మీ Python పనితీరును (performance) నిశ్శబ్దంగా దెబ్బతీస్తుంది.
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 మునుపటి లైన్ నుండే బ్యాక్గ్రౌండ్లో నడుస్తూనే ఉంటుంది. మీకు orders వేరియబుల్ అవసరమయ్యే సమయానికి, రెండవ రిక్వెస్ట్ ఇప్పటికే పూర్తయి ఉండవచ్చు. ఈ పద్ధతి JavaScriptలో ఎంత సహజంగా ఉంటుందంటే, చాలా మంది డెవలపర్లు దీనిని ఒక 'concurrency trick'గా కూడా భావించరు. async పనిచేసే విధానం ఇదే.
Python సర్ప్రైజ్: ఒక 'Cold Coroutine'
Python వేరే నిబంధనలను ఉపయోగిస్తుంది. Pythonలో మీరు async def ఫంక్షన్ను పిలిచినప్పుడు, మీరు ఎటువంటి పనిని ప్రారంభించరు. మీకు కేవలం ఒక coroutine ఆబ్జెక్ట్ మాత్రమే అందుతుంది. దీనిని కాగితంపై రాసిన వంటకం (recipe) లాగా అనుకోండి. పదార్థాలు జాబితా చేయబడ్డాయి, దశలు స్పష్టంగా ఉన్నాయి, కానీ ఓవెన్లో ఏదీ లేదు. ఏదైనా దానిని స్పష్టంగా event loop ద్వారా నడిపించే వరకు, అది నిష్క్రియంగానే (inert) ఉంటుంది.
ఇక్కడే ఒక ఉచ్చు ఉంది. ఒక యూజర్ మరియు వారి ఆర్డర్లు కావాలనుకునే JavaScript ఇంజనీర్, Pythonలో ఇలా రాసే అవకాశం ఉంది:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
ఇది కన్కరెంట్ (concurrent) లాగా కనిపిస్తుంది. కన్కరెంట్ లాగా అనిపిస్తుంది. కానీ ఇది పూర్తిగా సీక్వెన్షియల్ (sequential).
మొదటి లైన్ ఒక నిద్రాణమైన (dormant) coroutineని user_coroకి కేటాయిస్తుంది. రెండవ లైన్ మరొక నిద్రాణమైన coroutineని orders_coroకి కేటాయిస్తుంది. ఎగ్జిక్యూషన్ await user_coro కి చేరుకున్నప్పుడు, Python చివరకు మొదటి టాస్క్ను ప్రారంభించి, అది పూర్తయ్యే వరకు నడుపుతుంది. fetch_user పూర్తయిన తర్వాత మాత్రమే ఇంటర్ప్రెటర్ await orders_coro కి చేరుకుని రెండవ టాస్క్ను ప్రారంభిస్తుంది. మీ మొత్తం ఎగ్జిక్యూషన్ సమయం రెండు I/O ఆపరేషన్ల మొత్తం అవుతుంది, వాటిలో ఏది ఎక్కువ సమయం తీసుకుంటుందో అది కాదు. మీరు వాటిని పారలల్గా (parallel) నడపలేదు. మీరు వాటిని అదనపు దశలతో ఒకదాని తర్వాత ఒకటి నడపారు.
ఈ బగ్ ఎందుకు కనిపించదు
ఇది నెలల తరబడి కొనసాగే రకమైన పెర్ఫార్మెన్స్ రిగ్రెషన్ (performance regression). కోడ్ సరైన Python కోడ్. ఇది టైప్ చెకర్స్ను (type checkers) పాస్ అవుతుంది. ఇది సరైన ఫలితాలను ఇస్తుంది. కానీ ఇది సగం వేగంతో లేదా అంతకంటే తక్కువ వేగంతో నడుస్తుంది. ఎటువంటి స్టాక్ ట్రేస్ (stack trace) లేదా హెచ్చరిక (warning) లేకపోవడం వల్ల, ఇంజనీరింగ్ టీమ్లు తరచుగా ఇతర కారణాల కోసం వెతుకుతారు. వారు Redis క్యాచీలను (caches) జోడిస్తారు, డేటాబేస్ టైర్లను అప్గ్రేడ్ చేస్తారు లేదా హోస్టింగ్ రీజియన్లను మారుస్తారు. కానీ అసలు కారణం await నిజంగా ఏమి చేస్తుందనే విషయంలో ఉన్న చిన్నపాటి అంచనాల తేడా.
Pythonను నిజంగా కన్కరెంట్గా నడపడానికి మూడు మార్గాలు
దీనిని సరిచేయడానికి, మీరు Python యొక్క event loop కి పనిని వెంటనే షెడ్యూల్ చేయమని చెప్పాలి. మీకు కేవలం ఒక raw coroutine కంటే ఎక్కువ యాక్టివ్గా ఉండేది కావాలి. మీకు ఒక Task కావాలి.
1. asyncio.create_task
JavaScript పద్ధతికి అత్యంత ప్రత్యక్ష అనువాదం ఏమిటంటే, మీ coroutineని ఒక Taskలో చుట్టడం (wrap). మీరు ఒక Taskని సృష్టించిన వెంటనే అది event loopలో షెడ్యూల్ చేయబడుతుంది. ఇది కదులుతున్న 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 మీ కోసం ఆ పనిని సులభతరం చేస్తుంది. ఇది ప్రతి coroutineని అంతర్గతంగా ఒక Taskగా షెడ్యూల్ చేసి, వాటన్నింటినీ కలిపి await చేస్తుంది.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
ఇది క్లుప్తంగా మరియు చదవడానికి సులభంగా ఉంటుంది. ఆపరేషన్లు స్వతంత్రంగా ఉన్నప్పుడు మరియు "వీటన్నింటినీ నడుపు, ఆపై నాకు ప్రతి ఫలితాన్ని ఇవ్వు" అని చెప్పే ఒకే ఒక లైన్ కావాలనుకున్నప్పుడు ఇది బాగా ఉపయోగపడుతుంది. అలాగే, అంతర్గత టాస్క్లు వేర్వేరు క్రమంలో పూర్తయినప్పటికీ, ఇది తిరిగి ఇచ్చే లిస్ట్ (list) లేదా టపుల్ (tuple)లో ఆర్గ్యుమెంట్ల క్రమాన్ని అలాగే ఉంచుతుంది.
3. asyncio.TaskGroup
Python 3.11 TaskGroupను పరిచయం చేసింది, ఇది స్టాండర్డ్ లైబ్రరీకి స్ట్రక్చర్డ్ కన్కరెన్సీని (structured concurrency) తీసుకువస్తుంది. టాస్క్లను మాన్యువల్గా సృష్టించడానికి బదులుగా, మీరు ఒక కాంటెక్స్ట్ మేనేజర్ను ఉపయోగిస్తారు, ఇది ప్రతి టాస్క్ సరిగ్గా పూర్తయ్యేలా చూస్తుంది. ఒకవేళ ఒక టాస్క్ ఎక్సెప్షన్ను (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) కోసం అద్భుతమైనది. ఇది ఒక టాస్క్ అనాథగా (orphaning a Task) మిగిలిపోయే ప్రమాదాన్ని తొలగిస్తుంది మరియు సంబంధిత ఆపరేషన్ల లైఫ్ సైకిల్ను ఒకే లాజికల్ అంబ్రెల్లా కింద సమూహపరుస్తుంది. మీ కోడ్బేస్ Python 3.11 లేదా అంతకంటే కొత్త వెర్షన్లో నడుస్తుంటే, ఫ్యాన్-అవుట్ కన్కరెన్సీ (fan-out concurrency) కోసం ఇది తరచుగా అత్యంత క్లీన్ ఆర్కిటెక్చర్.
మెంటల్ మోడల్: await అంటే "దీన్ని ఇప్పుడే రన్ చేయి" అని అర్థం
దీనిలోని ప్రధాన పాఠం భాషా సంబంధితమైనది. JavaScriptలో, మీరు awaitను "అంతలో" (meanwhile) అని చదవవచ్చు. మీరు పనిని ప్రారంభిస్తారు, ఇతర పనులు చేస్తారు మరియు మీకు విలువ (value) అవసరమైనప్పుడు మాత్రమే ఆగుతారు. Pythonలో, await అంటే "ఈ కోరూటిన్ను (coroutine) దాని తదుపరి సస్పెన్షన్ పాయింట్ లేదా పూర్తి అయ్యే వరకు నడపండి" అని అర్థం. ఒకవేళ కోరూటిన్ ఇంకా షెడ్యూల్ చేయబడకపోతే, await దానిని షెడ్యూల్ చేస్తుంది. అందుకే మీరు రెండు రా (raw) కోరూటిన్లను ప్రారంభించి, తర్వాత వాటిని await చేయలేరు. ఎందుకంటే ఈ మధ్య కాలంలో మీరు ఈవెంట్ లూప్కు (event loop) చేయడానికి ఏమీ ఇవ్వలేదు.
Python కోరూటిన్లను జనరేటర్ ఫంక్షన్ల (generator functions) లాగా భావించండి. జనరేటర్ను కాల్ చేయడం వల్ల అది ఇటరేట్ (iterate) కాదు. మీరు దానిపై లూప్ చేయాలి, next()ని కాల్ చేయాలి లేదా దానిని ఒక కన్స్యూమర్కు పంపాలి. Async కూడా అదే విధంగా పనిచేస్తుంది. asyncio.create_task అనేది "దీన్ని ఇప్పుడే ఈవెంట్ లూప్లో ఉంచండి" అని చెప్పే కన్స్యూమర్. తదుపరి await కేవలం పని పూర్తయిన సిగ్నల్ కోసం వేచి ఉంటుంది.
సహాయపడే ఒక ముఖ్యమైన అలవాటు: మీరు ఎప్పుడైనా await లేకుండా ఒక async ఫంక్షన్ కాల్ను వేరియబుల్కు అసైన్ చేసినప్పుడు, మీరు దానిని షెడ్యూల్ చేశారా లేదా అని మిమ్మల్ని మీరు ప్రశ్నించుకోండి. ఒకవేళ కుడి వైపున ఉన్న భాగం create_task, gather, లేదా TaskGroupలో చుట్టబడి (wrapped) లేకపోతే, అది రన్ అవ్వదు. అది కేవలం కౌంటర్ మీద ఉన్న ఒక రెసిపీ (recipe) లాంటిది మాత్రమే.
ముగింపు (Takeaway)
Python యొక్క async రన్టైమ్ శక్తివంతమైనది, కానీ ఇది స్పష్టమైన ఉద్దేశాన్ని (explicit intent) కోరుతుంది. మీరు ఒక ఫంక్షన్ను కాల్ చేసినంత మాత్రాన భాష బ్యాక్గ్రౌండ్ పనిని ప్రారంభించదు. మీరు JavaScript నుండి వస్తుంటే, మీరు కోరూటిన్ను వేరియబుల్లో స్టోర్ చేసి, తర్వాత await చేసే ప్రతి చోట తనిఖీ (audit) చేయండి. మీరు దానిని మొదట ఒక Taskగా మార్చకపోతే, మీరు async దుస్తులు ధరించిన సీక్వెన్షియల్ కోడ్ను (sequential code) రాసినట్లే. పనిని ఒక Taskతో ప్రారంభించండి, ఆపై ఫలితాల కోసం వేచి ఉండండి. అలా మీరు Python asyncను ఒక నిశ్శబ్ద అడ్డంకి (silent bottleneck) నుండి నిజమైన కన్కరెన్సీ టూల్గా మార్చగలరు.
