JavaScript-லிருந்து Python-க்கு மாறுவது என்பது, அதே தெருக் குறிகளுடன் ஆனால் வெவ்வேறு போக்குவரத்து விதிகளைக் கொண்ட ஒரு நகரத்திற்கு மாறுவது போன்றது. இதன் தொடரியல் (syntax) எளிமையாகவும் பரிச்சயமானதாகவும் இருக்கும். இலக்கணத்திலேயே async மற்றும் await இருப்பதைப் பார்த்து, அதன் செயல்பாட்டு முறை (mental model) அப்படியே இருக்கும் என்று நீங்கள் நினைப்பீர்கள். ஆனால் அது உண்மையில்லை. JavaScript-ல் நீங்கள் பழகிய ஒரு முறை, உங்கள் Python செயல்திறனை (performance) எந்தத் தவறுச் செய்தியையும் (error) காட்டாமலும், code review-வில் சிக்காமல்லும் மெதுவாகச் செய்துவிடும்.
JavaScript உங்களுக்கு எப்படித் தொடங்கிவிட்டு மறந்துவிடக் கற்றுக்கொடுக்கிறது
JavaScript-ல், ஒரு async function அழைப்பு என்பது 'eager' (உடனடியாகச் செயல்படும் தன்மை கொண்டது). நீங்கள் அதை அழைத்த அடுத்த கணமே, engine ஒரு 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 முடியும் வரை தற்போதைய function-ஐ நிறுத்தி வைக்கும், ஆனால் fetchOrders முந்தைய வரியிலிருந்தே பின்னணியில் இயங்கிக் கொண்டிருக்கும். உங்களுக்கு orders மாறி (variable) தேவைப்படும்போது, இரண்டாவது கோரிக்கை ஏற்கனவே முடிந்து போயிருக்கலாம். இந்த முறை JavaScript-ல் மிகவும் இயல்பானது என்பதால், பல டெவலப்பர்கள் இதை ஒரு concurrency நுட்பமாகக்கூட நினைப்பதில்லை. இது async செயல்படும் விதம் மட்டுமே.
Python-ன் ஆச்சரியம்: ஒரு 'Cold Coroutine'
Python ஒரு மாறுபட்ட ஒப்பந்தத்தைப் பயன்படுத்துகிறது. Python-ல் நீங்கள் ஒரு async def function-ஐ அழைக்கும்போது, எந்த வேலையும் தொடங்காது. உங்களுக்கு ஒரு coroutine object மட்டுமே கிடைக்கும். இதை ஒரு காகிதத்தில் எழுதப்பட்ட சமையல் குறிப்பு போல நினைத்துக் கொள்ளுங்கள். தேவையான பொருட்கள் பட்டியலிடப்பட்டுள்ளன, செய்முறையும் தெளிவாக உள்ளது, ஆனால் அடுப்பில் எதுவும் சமைக்கப்படவில்லை. எதாவது ஒன்று அந்த coroutine-ஐ event loop வழியாக இயக்கினால் மட்டுமே அது செயல்படும், அதுவரை அது செயலற்ற நிலையில் இருக்கும்.
இங்கேதான் ஒரு சிக்கல் உள்ளது. ஒரு பயனர் மற்றும் அவரது ஆர்டர்களைத் தேவைப்படும் ஒரு JavaScript பொறியாளர், Python-ல் இப்படி எழுதக்கூடும்:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
இது ஒரே நேரத்தில் நடப்பது போலத் தோன்றும். ஆனால் இது முற்றிலும் வரிசைக்கிரமமானது (sequential).
முதல் வரி ஒரு செயலற்ற coroutine-ஐ user_coro-வில் சேமிக்கிறது. இரண்டாவது வரி மற்றொரு செயலற்ற coroutine-ஐ orders_coro-வில் சேமிக்கிறது. இயக்கம் (execution) await user_coro-வை அடையும்போது, Python இறுதியாக முதல் பணியைத் தொடங்கி அதை முடிக்கும் வரை இயங்கும். fetch_user முடிந்த பின்னரே interpreter await orders_coro-வை அடையும் மற்றும் இரண்டாவது பணியைத் தொடங்கும். உங்கள் மொத்தச் செயல்பாட்டு நேரம் (execution time) இரண்டு I/O செயல்பாடுகளின் கூட்டுத்தொகையாக இருக்கும், அதில் எது அதிக நேரம் எடுத்ததோ அது மட்டும் அல்ல. நீங்கள் அவற்றை இணையாக (parallel) இயக்கவில்லை. நீங்கள் அவற்றை ஒவ்வொன்றாக, கூடுதல் படிகளுடன் இயக்கினீர்கள்.
இந்த பிழை ஏன் கண்ணுக்குத் தெரிவதில்லை
இது பல மாதங்கள் வரை கண்டறியப்படாமல் இருக்கும் ஒரு செயல்திறன் குறைபாடு (performance regression). இந்த code சரியான Python code தான். இது type checkers-ஐத் தாண்டும். சரியான முடிவுகளையும் தரும். ஆனால் இது பாதி வேகத்திலோ அல்லது அதைவிடக் குறைவாகவோ இயங்கும். இதில் எந்த stack trace அல்லது எச்சரிக்கையும் (warning) இல்லாததால், பொறியியல் குழுக்கள் பெரும்பாலும் வேறு இடங்களையே முதலில் தேடுவார்கள். அவர்கள் Redis caches-ஐச் சேர்ப்பார்கள், database tiers-ஐ மேம்படுத்துவார்கள் அல்லது hosting regions-ஐ மாற்றுவார்கள். await உண்மையில் என்ன செய்கிறது என்பதில் உள்ள எதிர்பார்ப்புகளுக்கு இடையிலான ஒரு சிறிய முரண்பாடே இதற்குக் காரணம்.
Python-ஐ உண்மையில் ஒரே நேரத்தில் (concurrently) இயங்கச் செய்வதற்கான மூன்று வழிகள்
இதைச் சரிசெய்ய, Python-ன் event loop-க்கு வேலையை உடனடியாகத் திட்டமிடுமாறு (schedule) நீங்கள் கூற வேண்டும். ஒரு சாதாரண coroutine-ஐ விடச் சுறுசுறுப்பான ஒன்று உங்களுக்குத் தேவை. அதுதான் ஒரு Task.
1. asyncio.create_task
JavaScript முறையை நேரடியாகப் பின்பற்றுவது என்றால், உங்கள் coroutine-ஐ ஒரு Task-ஆக மாற்றுவதாகும். ஒரு 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 பற்றிய error-ஐக் காட்டும். நீங்கள் உங்கள் முடிவுகளை (results) கண்டிப்பாகச் சேகரிக்க வேண்டும்.
2. asyncio.gather
நீங்கள் அடுத்த கட்டத்திற்குச் செல்வதற்கு முன் பல coroutines முடிவடைய வேண்டும் என்றால், asyncio.gather அந்தத் தேவைகளை உங்களுக்காகச் செய்துவிடும். இது ஒவ்வொரு coroutine-ஐயும் உள்நாட்டிலேயே (internally) ஒரு Task-ஆகத் திட்டமிட்டு அவற்றை ஒன்றாக await செய்யும்.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
இது சுருக்கமானது மற்றும் எளிதில் வாசிக்கக்கூடியது. செயல்பாடுகள் ஒன்றையொன்று சார்ந்து இல்லாமல் இருக்கும்போது, "இவை அனைத்தையும் இயக்கு, பிறகு எனக்கு அனைத்து முடிவுகளையும் கொடு" என்று ஒரே வரியில் சொல்ல இது சிறந்தது. மேலும், அடிப்படையான பணிகள் (tasks) வேறு வரிசையில் முடிந்தாலும், இது திரும்பப் பெறப்படும் list அல்லது tuple-ல் உள்ள வாதங்களின் (arguments) வரிசையை அப்படியே பாதுகாக்கும்.
3. asyncio.TaskGroup
Python 3.11 TaskGroup-ஐ அறிமுகப்படுத்தியது, இது standard library-க்கு structured concurrency-யைக் கொண்டுவருகிறது. ஒவ்வொன்றையும் கைமுறையாக உருவாக்குவதற்குப் பதிலாக, நீங்கள் ஒரு context manager-ஐப் பயன்படுத்துகிறீர்கள்; இது உருவாக்கப்பட்ட ஒவ்வொரு task-உம் சரியாக முடிவடைவதை உறுதி செய்கிறது. ஒரு task விதிவிலக்கை (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-களுக்குச் சிறந்தது. இது ஒரு Task தனித்து விடப்படும் (orphaning) அபாயத்தை நீக்குகிறது, மேலும் தொடர்புடைய செயல்பாடுகளின் வாழ்க்கைச் சுழற்சியை (lifecycle) ஒரே தர்க்கரீதியான குடையின் கீழ் ஒருங்கிணைக்கிறது. உங்கள் codebase Python 3.11 அல்லது அதற்குப் பிந்தைய பதிப்பில் இயங்கினால், fan-out concurrency-க்கு இதுவே பெரும்பாலும் மிகத் தெளிவான கட்டமைப்பாக (architecture) இருக்கும்.
மனோபாவ மாதிரி (Mental Model): await என்பது "இதை இப்போதே இயக்கு" என்று பொருள்படும்
இதன் முக்கிய பாடம் மொழியியல் சார்ந்தது. JavaScript-இல், await-ஐ "இதற்கிடையில்" (meanwhile) என்று நீங்கள் புரிந்துகொள்ளலாம். நீங்கள் வேலையைத் தொடங்கிவிட்டு, மற்ற வேலைகளைச் செய்துவிட்டு, அந்த மதிப்பு (value) தேவைப்படும்போது மட்டும் நிறுத்தி வைப்பீர்கள். Python-இல், await என்பது "இந்த coroutine-ஐ அதன் அடுத்த நிறுத்தி வைக்கும் புள்ளி (suspension point) அல்லது நிறைவு நிலைக்குக் கொண்டு செல்லுங்கள்" என்று பொருள்படும். அந்த coroutine இன்னும் திட்டமிடப்படவில்லை (scheduled) என்றால், await தான் அதைத் திட்டமிடுகிறது. அதனால்தான் நீங்கள் இரண்டு raw coroutines-களைத் தொடங்கிவிட்டு, பின்னர் அவற்றை await செய்ய முடியாது. இதற்கிடையில் நீங்கள் event loop-க்குச் செய்ய ஏதேனும் ஒன்றைக் கொடுக்கவில்லை.
Python coroutines-களை generator functions போலக் கருதுங்கள். generator-ஐ அழைப்பது அதைத் தானாகச் சுழற்சி (iterate) செய்யாது. நீங்கள் அதன் மேல் loop செய்ய வேண்டும், next()-ஐ அழைக்க வேண்டும் அல்லது அதை ஒரு consumer-க்கு அனுப்ப வேண்டும். Async-உம் இதே வழியில் செயல்படுகிறது. asyncio.create_task என்பது "இதை இப்போதே event loop-இல் போடு" என்று சொல்லும் ஒரு consumer ஆகும். அதன் பிறகு வரும் await என்பது அந்த வேலை முடிவடைவதற்கான சிக்னலுக்காகக் காத்திருக்கிறது.
உதவும் ஒரு நடைமுறைப் பழக்கம்: எப்போதெல்லாம் நீங்கள் ஒரு async function அழைப்பை await இல்லாமல் ஒரு variable-க்கு ஒதுக்குகிறீர்களோ, அப்போதெல்லாம் அதை நீங்கள் திட்டமிட்டுள்ளீர்களா (scheduled) என்று உங்களையே கேட்டுக்கொள்ளுங்கள். வலதுபுறம் உள்ள பகுதி create_task, gather, அல்லது TaskGroup-க்குள் சுற்றப்பட்டிருக்கவில்லை என்றால், அது இயங்கவில்லை என்று அர்த்தம். அது மேடையின் மீது அப்படியே இருக்கும் ஒரு சமையல் குறிப்பைப் போன்றது.
முக்கியக் குறிப்பு (Takeaway)
Python-இன் async runtime வலிமையானது, ஆனால் அது தெளிவான நோக்கத்தைக் (explicit intent) கோருகிறது. நீங்கள் ஒரு function-ஐ அழைத்ததாலேயே அந்த மொழி பின்னணி வேலையைத் (background work) தொடங்குவதில்லை. நீங்கள் JavaScript-லிருந்து வருகிறீர்கள் என்றால், ஒரு coroutine-ஐ variable-இல் சேமித்துவிட்டு பின்னர் await செய்யும் ஒவ்வொரு இடத்தையும் ஆய்வு செய்யுங்கள். நீங்கள் அதை முதலில் ஒரு Task-ஆக மாற்றவில்லை என்றால், நீங்கள் async உடையணிந்த ஒரு sequential code-ஐத்தான் எழுதியிருக்கிறீர்கள். வேலையை ஒரு Task மூலம் தொடங்குங்கள், பின்னர் முடிவுகளுக்காகக் காத்திருங்கள். இதன் மூலம் தான் Python async-ஐ ஒரு அமைதியானத் தடையிலிருந்து (silent bottleneck) உண்மையான concurrency கருவியாக மாற்ற முடியும்.
