JavaScript થી Python પર સ્વિચ કરવું એ એવા શહેરમાં રહેવા જવા જેવું છે જ્યાં રસ્તાના સંકેતો તો એ જ છે પણ ટ્રાફિકના નિયમો અલગ છે. તેની સિન્ટેક્સ (syntax) મૈત્રીપૂર્ણ અને પરિચિત લાગે છે. તમે વ્યાકરણમાં async અને await ને ત્યાં જ જુઓ છો, તેથી તમે માની લો છો કે તમારો મેન્ટલ મોડલ (mental model) સરળતાથી કામ કરશે. પણ તે එવું નથી. JavaScript ની એક ટેવ તમારા Python પર્ફોર્મન્સને (performance) કોઈ પણ ક્રેશ (crash) વગર, કોઈ એરર (error) લોગ કર્યા વગર અને કોઈ ઝડપી કોડ રિવ્યુમાં દેખાયા વગર છૂપી રીતે ઘટાડી દે છે.

JavaScript તમને કેવી રીતે 'શરૂ કરો અને ભૂલી જાઓ' (Start and Forget) શીખવે છે

JavaScript માં, એક async ફંક્શન કોલ 'ઈગર' (eager) હોય છે. જે ક્ષણે તમે તેને ઇનવોક (invoke) કરો છો, એન્જિન એક 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 રિઝોલ્વ (resolve) ન થાય ત્યાં સુધી વર્તમાન ફંક્શનને સ્થગિત કરે છે, પરંતુ fetchOrders અગાઉની લાઇનથી જ બેકગ્રાઉન્ડમાં ચાલતું રહે છે. જ્યારે તમને orders વેરિએબલની જરૂર પડે, ત્યારે બીજી રિક્વેસ્ટ કદાચ પૂર્ણ થઈ ગઈ હોય. JavaScript માં આ પેટર્ન એટલી કુદરતી લાગે છે કે ઘણા ડેવલપર્સ તેને કન્કરન્સી (concurrency) ની ટ્રિક તરીકે પણ નથી જોતા. તે ફક્ત async કેવી રીતે કામ કરે છે તે જ છે.

Python નો આશ્ચર્ય: એક કોલ્ડ કોરૂટિન (Cold Coroutine)

Python એક અલગ કરાર (contract) વાપરે છે. જ્યારે તમે Python માં async def ફંક્શનને કોલ કરો છો, ત્યારે તમે કોઈ કામ શરૂ નથી કરતા. તમને એક કોરૂટિન (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) છે.

પહેલી લાઇન user_coro ને એક સુષુપ્ત (dormant) કોરૂટિન અસાઇન કરે છે. બીજી લાઇન orders_coro ને બીજું સુષુપ્ત કોરૂટિન અસાઇન કરે છે. જ્યારે એક્ઝિક્યુશન await user_coro પર પહોંચે છે, ત્યારે Python આખરે પ્રથમ કાર્ય શરૂ કરે છે અને તેને પૂર્ણ કરે છે. fetch_user પૂરું થયા પછી જ ઇન્ટરપ્રિટર await orders_coro સુધી પહોંચે છે અને બીજું કાર્ય શરૂ કરે છે. તમારો કુલ એક્ઝિક્યુશન સમય બંને I/O ઓપરેશન્સનો સરવાળો છે, સૌથી લાંબો સમય નથી. તમે તેમને પેરેલલ (parallel) રીતે ચલાવ્યા નથી. તમે તેમને વધારાના સ્ટેપ્સ સાથે એક પછી એક ચલાવ્યા છે.

આ બગ (bug) શા માટે અદ્રશ્ય છે

આ પ્રકારનું પર્ફોર્મન્સ રિગ્રેશન (performance regression) મહિનાઓ સુધી ટકી રહે છે. કોડ માન્ય Python છે. તે ટાઇપ ચેકર્સ (type checkers) પાસ કરે છે. તે સાચા પરિણામો આપે છે. તે ફક્ત અડધી ઝડપે, અથવા તેનાથી પણ ધીમે ચાલે છે. કારણ કે કોઈ સ્ટેક ટ્રેસ (stack trace) કે ચેતવણી નથી, તેથી એન્જિનિયરિંગ ટીમો ઘણીવાર પહેલા અન્ય જગ્યાએ તપાસ કરે છે. તેઓ Redis કેશ (caches) ઉમેરે છે, ડેટાબેઝ ટાયર્સ અપગ્રેડ કરે છે, અથવા હોસ્ટિંગ રિજીયન બદલે છે. સાચો દોષી await ખરેખર શું કરે છે તે વિશેની અપેક્ષાઓમાં રહેલો એક સૂક્ષ્મ તફાવત છે.

Python ને ખરેખર કન્કરન્ટલી (concurrently) ચલાવવા માટેના ત્રણ રસ્તા

આને સુધારવા માટે, તમારે Python ના ઇવેન્ટ લૂપને કામ તરત જ શેડ્યૂલ કરવા માટે કહેવું પડશે. તમારે કાચા કોરૂટિન કરતા કંઈક વધુ સક્રિય જોઈએ છે. તમારે એક 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 ડિસ્ટ્રોય થયેલ પેન્ડિંગ Task (destroyed pending task) વિશે એરર આપશે. તમારે હજુ પણ તમારા પરિણામો મેળવવા જ પડશે.

2. asyncio.gather

જો તમારી પાસે ઘણા બધા કોરૂટિન હોય જે તમારે આગળ વધતા પહેલા પૂરું કરવાની જરૂર હોય, તો asyncio.gather તમારા માટે બુઈલરપ્લેટ (boilerplate) હેન્ડલ કરે છે. તે આંતરિક રીતે દરેક કોરૂટિનને Task તરીકે શેડ્યૂલ કરે છે અને તેમને સાથે await કરે છે.

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

આ સંક્ષિપ્ત અને વાંચવા યોગ્ય છે. જ્યારે ઓપરેશન્સ સ્વતંત્ર હોય અને તમે એક એવી લાઇન ઈચ્છતા હોવ જે "આ બધું ચલાવો, પછી મને દરેક પરિણામ આપો" તે વ્યક્ત કરે ત્યારે તે શ્રેષ્ઠ કામ કરે છે. તે વળતરી આપેલ લિસ્ટ અથવા ટ્યુપલમાં આર્ગ્યુમેન્ટ્સનો ક્રમ પણ જાળવી રાખે છે, ભલે અન્ડરલાઇંગ ટાસ્ક અલગ ક્રમમાં પૂર્ણ થાય.

3. asyncio.TaskGroup

Python 3.11 એ TaskGroup રજૂ કર્યું છે, જે સ્ટાન્ડર્ડ લાઈબ્રેરીમાં સ્ટ્રક્ચર્ડ કન્કરન્સી (structured concurrency) લાવે છે. હાથથી ટાસ્ક (tasks) બનાવવાને બદલે, તમે કોન્ટેક્સ્ટ મેનેજરનો ઉપયોગ કરો છો જે ખાતરી કરે છે કે દરેક બનાવેલ ટાસ્ક યોગ્ય રીતે પૂર્ણ થાય. જો એક ટાસ્ક એક્સેપ્શન (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()

આ પેટર્ન જટિલ વર્કફ્લો માટે ઉત્તમ છે. તે ટાસ્કને અનાથ (orphaning) બનાવવાનું જોખમ દૂર કરે છે, અને સંબંધિત કામગીરીના જીવનચક્રને (lifecycle) એક તાર્કિક છત્ર હેઠળ જૂથબદ્ધ કરે છે. જો તમારો કોડબેઝ Python 3.11 અથવા તેનાથી નવા વર્ઝન પર ચાલે છે, તો ફેન-આઉટ કન્કરન્સી (fan-out concurrency) માટે આ ઘણીવાર સૌથી સ્વચ્છ આર્કિટેક્ચર છે.

માનસિક મોડેલ: await નો અર્થ છે "આને અત્યારે જ ચલાવો"

મુખ્ય પાઠ ભાષાકીય છે. JavaScript માં, તમે await ને "તે દરમિયાન" (meanwhile) તરીકે વાંચી શકો છો. તમે કામ શરૂ કરો છો, અન્ય વસ્તુઓ કરો છો, અને જ્યારે તમને કિંમત (value) ની જરૂર હોય ત્યારે જ અટકી જાઓ છો. Python માં, await નો અર્થ છે "આ કોરૂટિન (coroutine) ને તેના આગામી સસ્પેન્શન પોઈન્ટ અથવા પૂર્ણતા સુધી લઈ જવું." જો કોરૂટિન હજુ સુધી શેડ્યૂલ કરવામાં આવ્યું નથી, તો await જ તેને શેડ્યૂલ કરે છે. એટલા માટે જ તમે બે કાચા (raw) કોરૂટિન શરૂ કરીને પછીથી તેને await કરી શકતા નથી. તમે તે દરમિયાન ઇવેન્ટ લૂપને કરવા માટે કંઈ આપ્યું નથી.

Python કોરૂટિનને જનરેટર ફંક્શન્સ (generator functions) ની જેમ વિચારો. જનરેટરને કોલ કરવાથી તે ઇટરેટ (iterate) થતું નથી. તમારે તેના પર લૂપ ચલાવવી પડશે, next() કોલ કરવું પડશે, અથવા તેને કન્ઝ્યુમર (consumer) ને આપવું પડશે. Async પણ આ જ રીતે કામ કરે છે. asyncio.create_task એ એવો કન્ઝ્યુમર છે જે કહે છે "આને અત્યારે જ ઇવેન્ટ લૂપ પર મૂકો." ત્યારબાદનું await ફક્ત પૂર્ણતાના સિગ્નલની રાહ જુએ છે.

મદદરૂપ થતી એક ચોક્કસ આદત: જ્યારે પણ તમે await વગર કોઈ async ફંક્શન કોલને વેરિયેબલમાં અસાઇન કરો છો, ત્યારે તમારી જાતને પૂછો કે શું તમે તેને શેડ્યૂલ કર્યું છે. જો જમણી બાજુ create_task, gather, અથવા TaskGroup માં વળેલું (wrapped) ન હોય, તો તે ચાલી રહ્યું નથી. તે ફક્ત કાઉન્ટર પર પડેલી એક રેસીપી જેવું છે.

મુખ્ય વાત (Takeaway)

Python નું async રનટાઇમ શક્તિશાળી છે, પરંતુ તે સ્પષ્ટ ઈરાદાની માંગ કરે છે. તમે ફક્ત કોઈ ફંક્શન કોલ કર્યું છે એટલે ભાષા બેકગ્રાઉન્ડમાં કામ શરૂ કરી દેતી નથી. જો તમે JavaScript માંથી આવતા હોવ, તો જ્યાં પણ તમે કોરૂટિનને વેરિયેબલમાં સ્ટોર કરો છો અને પછીથી તેને await કરો છો, તે દરેક જગ્યાની તપાસ (audit) કરો. જ્યાં સુધી તમે તેને પહેલા Task માં રૂપાંતરિત ન કર્યું હોય, ત્યાં સુધી તમે async ના વેશમાં સિક્વન્શિયલ (sequential) કોડ લખ્યો છે. કામને Task સાથે શરૂ કરો, પછી પરિણામોની રાહ જુઓ. આ રીતે તમે Python async ને સાયલન્ટ બોટલનેક (silent bottleneck) માંથી સાચા કન્કરન્સી ટૂલમાં બદલી શકો છો.