JavaScript-ൽ നിന്ന് Python-ലേക്ക് മാറുന്നത് ഒരേ തെരുവുചിഹ്നങ്ങളുള്ള എന്നാൽ വ്യത്യസ്ത ട്രാഫിക് നിയമങ്ങളുള്ള ഒരു നഗരത്തിലേക്ക് മാറുന്നത് പോലെയാണ്. സിന്റാക്സ് (syntax) പരിചിതമായി തോന്നാം. async, await എന്നിവ ഗ്രാമറിൽ നേരിട്ട് കാണുമ്പോൾ, പഴയ രീതി തന്നെ ഇവിടെയും പ്രവർത്തിക്കുമെന്ന് നമ്മൾ കരുതും. എന്നാൽ അത് ശരിയല്ല. JavaScript-ലെ ഒരു ശീലമായ പാറ്റേൺ, നിങ്ങളുടെ Python പെർഫോമൻസിനെ (performance) തകരാതെ തന്നെ പതുക്കെ താഴ്ത്തിക്കളയും; അത് എറർ (error) കാണിക്കില്ല, ലോഗ് (log) ചെയ്യുകയുമില്ല, ഒരു കോഡ് റിവ്യൂവിലും പെട്ടെന്ന് പിടിക്കപ്പെടുകയുമില്ല.
JavaScript നിങ്ങളെ എങ്ങനെ തുടങ്ങാനും മറന്നുപോകാനും പഠിപ്പിക്കുന്നു
JavaScript-ൽ, ഒരു async function call "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 പൂർത്തിയാകുന്നത് വരെ നിലവിലെ ഫങ്ക്ഷനെ നിർത്തിവെക്കുന്നു, എന്നാൽ fetchOrders തൊട്ടുമുമ്പത്തെ വരി മുതൽ പശ്ചാത്തലത്തിൽ പ്രവർത്തിച്ചുകൊണ്ടിരിക്കുകയാണ്. നിങ്ങൾക്ക് orders വേരിയബിൾ ആവശ്യമായി വരുമ്പോഴേക്കും രണ്ടാമത്തെ റിക്വസ്റ്റ് കഴിഞ്ഞിട്ടുണ്ടാകാം. ഈ പാറ്റേൺ JavaScript-ൽ വളരെ സ്വാഭാവികമായതിനാൽ, പല ഡെവലപ്പർമാരും ഇതിനെ ഒരു concurrency trick ആയി പോലും കാണാറില്ല. അത് async പ്രവർത്തിക്കുന്ന രീതി അങ്ങനെയാണ്.
Python നൽകുന്ന സർപ്രൈസ്: ഒരു Cold Coroutine
Python മറ്റൊരു രീതിയാണ് ഉപയോഗിക്കുന്നത്. Python-ൽ ഒരു async def ഫങ്ക്ഷൻ വിളിക്കുമ്പോൾ, നിങ്ങൾ ഒരു ജോലിയും ആരംഭിക്കുന്നില്ല. പകരം നിങ്ങൾക്ക് ഒരു coroutine object ആണ് ലഭിക്കുന്നത്. ഇതിനെ കടലാസിൽ എഴുതിയ ഒരു പാചകക്കുറിപ്പ് (recipe) പോലെ കരുതുക. ചേരുവകൾ ലിസ്റ്റ് ചെയ്തിട്ടുണ്ട്, ഘട്ടങ്ങൾ വ്യക്തമാണ്, പക്ഷേ അടുപ്പിൽ ഒന്നും പാകം ചെയ്യപ്പെടുന്നില്ല. എന്തെങ്കിലും ഒരു ഘടകം ആ coroutine-നെ event loop വഴി പ്രവർത്തിപ്പിക്കുന്നത് വരെ അത് നിഷ്ക്രിയമായിരിക്കും.
ഇവിടെയാണ് കെണി. ഒരു യൂസറെയും അവരുടെ ഓർഡറുകളെയും ആവശ്യമായ ഒരു JavaScript എഞ്ചിനീയർ Python-ൽ ഇപ്രകാരം എഴുതിയേക്കാം:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
ഇത് കാണാൻ concurrent ആണെന്ന് തോന്നും. എന്നാൽ ഇത് പൂർണ്ണമായും sequential ആണ്.
ആദ്യത്തെ വരി ഒരു നിഷ്ക്രിയമായ coroutine-നെ user_coro-ലേക്ക് നൽകുന്നു. രണ്ടാമത്തെ വരി മറ്റൊരു നിഷ്ക്രിയമായ coroutine-നെ orders_coro-ലേക്ക് നൽകുന്നു. എക്സിക്യൂഷൻ (execution) await user_coro-ൽ എത്തുമ്പോൾ, Python ഒടുവിൽ ആദ്യത്തെ ടാസ്ക് (task) ആരംഭിക്കുകയും അത് പൂർത്തിയാക്കുന്നത് വരെ പ്രവർത്തിപ്പിക്കുകയും ചെയ്യുന്നു. fetch_user പൂർത്തിയായതിന് ശേഷം മാത്രമേ ഇന്റർപ്രെറ്റർ (interpreter) await orders_coro-ൽ എത്തുകയും രണ്ടാമത്തെ ടാസ്ക് ആരംഭിക്കുകയും ചെയ്യുന്നുള്ളൂ. നിങ്ങളുടെ ആകെ എക്സിക്യൂഷൻ സമയം രണ്ട് I/O ഓപ്പറേഷനുകളുടെയും തുകയായിരിക്കും, അവയിൽ ഏറ്റവും കൂടുതൽ സമയം എടുക്കുന്നതിന്റേതല്ല. നിങ്ങൾ അവയെ പാരലൽ (parallel) ആയിട്ടല്ല പ്രവർത്തിപ്പിച്ചത്. പകരം അധിക ഘട്ടങ്ങളോടെ ഒന്നിനുപുറകെ ഒന്നായിട്ടാണ് പ്രവർത്തിപ്പിച്ചത്.
ഈ ബഗ്ഗ് എന്തുകൊണ്ട് കാണാൻ കഴിയില്ല?
മാസങ്ങളോളം നിലനിൽക്കാവുന്ന തരത്തിലുള്ള ഒരു performance regression ആണ് ഇത്. കോഡ് ശരിയായ Python ആണ്. അത് type checkers പാസ്സ് ചെയ്യുന്നു. ശരിയായ ഫലങ്ങൾ നൽകുന്നു. പക്ഷേ അത് പകുതി വേഗതയിൽ അല്ലെങ്കിൽ അതിലും കുറഞ്ഞ വേഗതയിൽ മാത്രമേ പ്രവർത്തിക്കൂ. സ്റ്റാക്ക് ട്രാസ് (stack trace) ഇല്ലാത്തതിനാലും മുന്നറിയിപ്പുകൾ ലഭിക്കാത്തതിനാലും, എഞ്ചിനീയറിംഗ് ടീമുകൾ പലപ്പോഴും മറ്റ് കാരണങ്ങളാണ് ആദ്യം പരിശോധിക്കുന്നത്. അവർ Redis കാഷെകൾ (caches) ചേർക്കുന്നു, ഡാറ്റാബേസ് ടയറുകൾ അപ്ഗ്രേഡ് ചെയ്യുന്നു, അല്ലെങ്കിൽ ഹോസ്റ്റിംഗ് റീജിയനുകൾ മാറ്റുന്നു. എന്നാൽ await യഥാർത്ഥത്തിൽ എന്താണ് ചെയ്യുന്നത് എന്നതിനെക്കുറിച്ചുള്ള തെറ്റായ ധാരണയാണ് ഇതിന്റെ യഥാർത്ഥ കാരണം.
Python-നെ ശരിക്കും Concurrently പ്രവർത്തിപ്പിക്കാൻ മൂന്ന് വഴികൾ
ഇത് പരിഹരിക്കാൻ, ജോലികൾ ഉടൻ തന്നെ ഷെഡ്യൂൾ ചെയ്യാൻ നിങ്ങൾ Python-ന്റെ event loop-നോട് പറയണം. ഒരു സാധാരണ 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 ചെയ്യാതിരിക്കുകയും ചെയ്താൽ, ഒരു destroyed pending task എന്നതിനെക്കുറിച്ച് Python ഒരു എറർ കാണിക്കും. നിങ്ങൾ ഫലങ്ങൾ ശേഖരിക്കേണ്ടതുണ്ട്.
2. asyncio.gather
നിങ്ങൾക്ക് മുന്നോട്ട് പോകുന്നതിന് മുമ്പ് ഒന്നിലധികം coroutines പൂർത്തിയാക്കേണ്ടതുണ്ടെങ്കിൽ, asyncio.gather ആ ജോലി എളുപ്പമാക്കുന്നു. ഇത് ഓരോ coroutine-നെയും ആന്തരികമായി ഒരു Task ആയി ഷെഡ്യൂൾ ചെയ്യുകയും അവയെ ഒരുമിച്ച് await ചെയ്യുകയും ചെയ്യുന്നു.
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
ഇത് ലളിതവും വായിക്കാൻ എളുപ്പവുമാണ്. ഓപ്പറേഷനുകൾ സ്വതന്ത്രമാണെന്നും "ഇവയെല്ലാം പ്രവർത്തിപ്പിക്കുക, എന്നിട്ട് എല്ലാ ഫലങ്ങളും എനിക്ക് നൽകുക" എന്ന് ഒറ്റ വരിയിൽ പ്രകടിപ്പിക്കണമെന്നും നിങ്ങൾ ആഗ്രഹിക്കുമ്പോഴാണ് ഇത് കൂടുതൽ ഉപകരിക്കുന്നത്. ടാസ്ക്കുകൾ വ്യത്യസ്ത ക്രമത്തിൽ പൂർത്തിയായാലും, തിരികെ ലഭിക്കുന്ന ലിസ്റ്റിലോ ട്യൂപ്പിളിലോ (tuple) ആർഗ്യുമെന്റുകളുടെ ക്രമം ഇത് നിലനിർത്തുന്നു.
3. asyncio.TaskGroup
Python 3.11 TaskGroup അവതരിപ്പിച്ചു, ഇത് സ്റ്റാൻഡേർഡ് ലൈബ്രറിയിലേക്ക് സ്ട്രക്ചേർഡ് കൺകറൻസി (structured concurrency) കൊണ്ടുവരുന്നു. ടാസ്ക്കുകൾ നേരിട്ട് നിർമ്മിക്കുന്നതിന് പകരം, ഓരോ ടാസ്ക്കും ശരിയായി പൂർത്തിയാകുന്നുവെന്ന് ഉറപ്പാക്കുന്ന ഒരു കോൺടെക്സ്റ്റ് മാനേജർ (context manager) നിങ്ങൾക്ക് ഉപയോഗിക്കാം. ഒരു ടാസ്ക് എക്സെപ്ഷൻ (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) പോകുന്ന സാഹചര്യം ഒഴിവാക്കുന്നു, കൂടാതെ ബന്ധപ്പെട്ട പ്രവർത്തനങ്ങളുടെ ലൈഫ്സൈക്കിളിനെ (lifecycle) ഒരൊറ്റ യുക്തിപരമായ പരിധിയിൽ (logical umbrella) കൊണ്ടുവരുന്നു. നിങ്ങളുടെ കോഡ്ബേസ് Python 3.11 അല്ലെങ്കിൽ പുതിയ പതിപ്പിലാണെങ്കിൽ, ഫാൻ-ഔട്ട് കൺകറൻസിക്ക് (fan-out concurrency) ഏറ്റവും മികച്ച ആർക്കിടെക്ചർ ഇതാണ്.
മെന്റൽ മോഡൽ: await എന്നാൽ "ഇത് ഇപ്പോൾ പ്രവർത്തിപ്പിക്കുക" എന്നാണ് അർത്ഥം
ഇതിലെ പ്രധാന പാഠം ഭാഷാപരമായതാണ്. JavaScript-ൽ, await-നെ "അതേസമയം" (meanwhile) എന്ന് വായിക്കാം. നിങ്ങൾ ഒരു ജോലി തുടങ്ങുന്നു, മറ്റ് കാര്യങ്ങൾ ചെയ്യുന്നു, മൂല്യം (value) ആവശ്യമുള്ളപ്പോൾ മാത്രം നിർത്തിവെക്കുന്നു. എന്നാൽ Python-ൽ, await എന്നാൽ "ഈ കോറൗട്ടിനെ (coroutine) അതിന്റെ അടുത്ത സസ്പെൻഷൻ പോയിന്റിലേക്കോ (suspension point) അല്ലെങ്കിൽ പൂർത്തീകരണത്തിലേക്കോ എത്തിക്കുക" എന്നാണ് അർത്ഥം. കോറൗട്ടിന് ഇതുവരെ ഷെഡ്യൂൾ ചെയ്തിട്ടില്ലെങ്കിൽ, await ആണ് അത് ഷെഡ്യൂൾ ചെയ്യുന്നത്. അതുകൊണ്ടാണ് രണ്ട് കോറൗട്ടിനുകൾ നേരിട്ട് സ്റ്റാർട്ട് ചെയ്ത ശേഷം അവയെ പിന്നീട് await ചെയ്യാൻ കഴിയാത്തത്. ആ ഇടവേളയിൽ ഇവന്റിലൂപ്പിന് (event loop) ചെയ്യാൻ നിങ്ങൾ ഒന്നും നൽകിയിട്ടില്ല.
Python കോറൗട്ടിനുകളെ ജനറേറ്റർ ഫംഗ്ഷനുകൾ (generator functions) പോലെ കരുതുക. ജനറേറ്റർ വിളിക്കുന്നത് അത് ഇറ്ററേറ്റ് (iterate) ചെയ്യാൻ സഹായിക്കില്ല. നിങ്ങൾ അതിലൂടെ ലൂപ്പ് ചെയ്യുകയോ, next() വിളിക്കുകയോ, അല്ലെങ്കിൽ ഒരു കൺസ്യൂമറിന് (consumer) കൈമാറുകയോ വേണം. Async-ഉം ഇതേ രീതിയിലാണ് പ്രവർത്തിക്കുന്നത്. "ഇത് ഇപ്പോൾ തന്നെ ഇവന്റിലൂപ്പിൽ ഇടുക" എന്ന് പറയുന്ന കൺസ്യൂമർ ആണ് asyncio.create_task. അതിനുശേഷമുള്ള await എന്നത് ജോലി പൂർത്തിയായതിന്റെ സിഗ്നലിനായി കാത്തിരിക്കുക മാത്രമാണ് ചെയ്യുന്നത്.
സഹായിക്കുന്ന ഒരു ശീലം: await ഇല്ലാതെ ഒരു async ഫംഗ്ഷൻ കോൾ ഒരു വേരിയബിളിലേക്ക് അസൈൻ ചെയ്യുമ്പോൾ, നിങ്ങൾ അത് ഷെഡ്യൂൾ ചെയ്തിട്ടുണ്ടോ എന്ന് സ്വയം ചോദിക്കുക. വലതുവശത്തുള്ള ഭാഗം create_task, gather, അല്ലെങ്കിൽ TaskGroup എന്നിവയിൽ ഉൾപ്പെടുത്തിയിട്ടില്ലെങ്കിൽ, അത് പ്രവർത്തിക്കുന്നില്ല. അത് കൗണ്ടറിൽ വെച്ചിരിക്കുന്ന ഒരു റെസിപ്പി പോലെ മാത്രമാണ്.
സംഗ്രഹം (Takeaway)
Python-ന്റെ async റൺടൈം ശക്തമാണ്, പക്ഷേ അതിന് വ്യക്തമായ ഉദ്ദേശ്യം ആവശ്യമാണ്. നിങ്ങൾ ഒരു ഫംഗ്ഷൻ വിളിച്ചു എന്നതുകൊണ്ട് മാത്രം ഭാഷ പശ്ചാത്തല ജോലികൾ (background work) ആരംഭിക്കില്ല. നിങ്ങൾ JavaScript-ൽ നിന്ന് വരുന്നവരാണെങ്കിൽ, ഒരു കോറൗട്ടിനെ വേരിയബിളിൽ സൂക്ഷിക്കുകയും പിന്നീട് await ചെയ്യുകയും ചെയ്യുന്ന ഓരോ സ്ഥലവും പരിശോധിക്കുക. നിങ്ങൾ അതിനെ ആദ്യം ഒരു Task ആയി മാറ്റുന്നില്ലെങ്കിൽ, നിങ്ങൾ എഴുതിയത് async വേഷം കെട്ടിയ സീക്വൻഷ്യൽ കോഡ് (sequential code) മാത്രമാണ്. ഒരു Task ഉപയോഗിച്ച് ജോലി ആരംഭിക്കുക, തുടർന്ന് ഫലങ്ങൾക്കായി കാത്തിരിക്കുക. അങ്ങനെയാണ് നിങ്ങൾക്ക് Python async-നെ ഒരു നിശബ്ദ തടസ്സത്തിൽ (bottleneck) നിന്ന് യഥാർത്ഥ കൺകറൻസി ടൂളായി മാറ്റാൻ കഴിയുന്നത്.
