یک بار برای همیشه asynchronous را یاد بگیریم!

یکی از مواردی که تازه‌کارهای برنامه‌نویسی بهش برمیخورن، اصطلاح asynchronous هست. اصطلاحی که ممکنه توی دید اول خیلی واضح نباشه و وقتی کار به syntax و ... میرسه اوضاع پیچیده‌تر میشه. توی این مقاله اول قراره یکی از این syntaxها رو خیلی سریع بررسی کنیم و بعد از اون، سراغ قسمت اصلی مقاله یعنی تمام اتفاقای جذابی که موقع استفاده از asynchronous رخ میده خواهیم رفت.

با این تیکه کد شروع میکنیم تا یه Promise ساده دریافت کنیم. احتمالا بدونین که تابع async همیشه بدون استثنا Promise برمیگردونه. یعنی باید منتظر یه اتفاقی بمونه. اون اتفاق میتونه response با کد 200 باشه یا ارور یا اتمام یه تایمر یا کلا هرچیز دیگه‌ای که باید براش منتظر بمونیم.

و اما کلمه کلیدی await: قبل از هرچیزی لازمه این نکته رو به خاطر بسپارین که کلمه کلیدی await فقط و فقط در توابع async قابل استفاده‌ست. نه هیچ جای دیگه! این تیکه کد رو خوب نگاه کنین:

پس await توی تیکه کد بالا صبر میکنه تا نتیجه مورد نظرمون دریافت بشه. یه نکته خیلی مهم رو دقت کنین که await نمیاد CPU رو به خاطر این قضیه درگیر کنه و کلا همه کارا رو متوقف کنه تا جواب بهش برگردونده بشه بلکه بقیه کارا همزمان انجام میشن. اثبات این موضوعم توی ادامه مقاله قراره انجام بدیم?

و اما قسمت جذاب ماجرا

تا حالا به این موضوع فکر کردین که وقتی دارین از توابع asynchronous استفاده میکنین، چه اتفاقی داره رخ میده؟ اصلا به درد میخوره یاد بگیریم؟ شکککک نکنین توی خیلی از رفع باگ‌ها کمک میکنه و فرشته نجات میشه! پس قراره باهم یاد بگیریمش :)

قبل از شروع بحثمون، دو تا اصطلاح رو باید معرفی کنم. دوتا اصطلاحی که خیلیامون توی inspect مرورگرامون دیدیم، اما معنیشو نمی‌دونیم:

  1. Call Stack | 2. Callback Queue

با یه کد ساده شروع میکنیم و کم‌کم به سمت asynchronous پیش میریم.

یادتون باشه اگر از asynchronous استفاده نکنیم، از مورد دومی یعنی Callback Queue هم استفاده نکردیم.

اولین مثالمون asynchronous نیست و یه تابع عادیه. پس طبق نکته بالا از Callback Queue استفاده‌ای نمیشه. میخوایم ببینیم چه اتفاقی توی Call Stack داره رخ میده:

یه تیکه کد خیلی ساده!
یه تیکه کد خیلی ساده!

خب بریم سراغ Call Stack برای این تیکه کد. هرکدوم از pushها رو توی موارد جداگونه و به صورت boldشده اوردم:

1- اولین چیزی که داخل این استک push میشه، تابعیه به اسم «main». دقیقا شبیه ++C که همچین تابعی رو به عنوان بدنه و فانکشن اصلی میشناخت ولی اینجا از نوع پنهان‌شدش? main پوش میشه.

2- خط به خط جلو میره تا خط آخر یعنی جایی که تابع «testCallStack» صدا زده شده. توی استک روی main، این تابع push میشه.

3- وارد testCallStack میشه. به تابع «forEach» میرسه. صداش میزنه و داخل استک روی main و testCallStack پوش میکنه.

4- داخل تابع forEach، از anonymous function استفاده شده. پس یه همچین چیزی داخل استک پوش میشه:

anonymous('Reza') <- اولین رشته‌ای که دریافت میکنه

5- و آخرین چیزی که بهش میرسه رو داخل کنسول چاپ و داخل استک push میکنه. یعنی:

console.log('Reza')

دو مورد آخری یعنی مورد 4 و 5، pop میشن و دوباره همین مراحل برای string بعدی آرایه تکرار و push میشه. و در آخر همه این موارد به ترتیب pop میشن و Call Stack خالی میشه.

و اما برای توابع asynchronous چه اتفاقایی رخ میده؟

میرسیم به قول اول مقاله... وقتی از asynchronous استفاده میکنیم دقیقا چه اتفاقایی داره میفته؟ تیکه کد خیلی ساده زیر رو دقیق بررسی کنیم تا همه چیزا برامون روشن بشه:

قبل این که بخوایم بررسی کنیم، بد نیست روی خروجی یه مقداری فکر کنین و جلوتر جوابتون رو ارزیابی کنین. به نظرتون کدوم خروجی رو خواهیم داشت؟

'I can not wait!' / 'Zero seconds' / 'I am done!' / 'Two seconds' or

'I can not wait!' / 'I am done!' / 'Zero seconds' / 'Two seconds' or

'I can not wait!' / 'Two seconds' / 'Zero seconds' / 'I am done!'




توی مثال ساده asynchronous بالا، میخوایم ببینیم چه اتفاقایی توی Call Stack و Callback Queue رخ میده. به ترتیب بررسی میکنیم و جلو میریم.

مثل مثال قبلی، اولین چیزی که داخل Call Stack پوش میشه، تابع «main» هست. بعد از اون، باز هم طبق مثال قبل، console.log اولی، پوش و بعد از چاپ در کنسول، pop میشه. پس تا اینجای کار این خروجی‌ها رو توی کنسولمون داریم:

'I can not wait!'

و اما قسمت جذاب :)

به console.log دومی میرسه. میبینه تایمر داره. اگر توی Call Stack پوش کنه، نمیتونه بعدا به این console.log برگرده. در لحظه هم که نمیتونه استفاده کنه و باید ۲ثانیه صبر کنه. طبق چیزی هم که اول مقاله گفتم، CPU این ۲ثانیه رو صبر نمیکنه و بقیه تسک‌ها رو همزمان باهاش انجام میده. پس فرشته نجات ما این وسط کیه؟

قبل از این که بریم سراغ جواب سوال بالا، لازمه یه نکته خیلی مهم رو درمورد جاوا اسکریپت ذکر کنم که فرضیه کمک‌گرفتن CPU از هسته دیگه برای تسک‌های همزمان رو درجا باطل میکنه:

جاوا اسکریپت فقط و فقط و فقط یک thread دارد!

پس تا اینجای کار برامون قطعی شد که نمیتونیم اون تایمر رو توی Call Stack پوش کنیم که مثلا توی thread جداگونه بهش برسیم.

این که CPU رو متوقف کنیم هم که معلومه کار درستی نیست! فکر کنین برای یه response از api اگر سرور دیر جواب میداد، هیچی توی صفحه وبمون نمایش داده نمیشد! درحالی که میدونیم اینجور نیست :)

پس چه شگردی به کار برده شده تا بدون استفاده از thread و توقف CPU، تسک‌های بعدی مثل ساعت کار کنن؟

جواب: APIهای نوشته شده براِی نود جی‌اس + Callback Queue

اما چه جوری؟ برمیگردیم به مراحلی که داشتیم دونه به دونه میرفتیم. وقتی به خط کد console.log دومی میرسیم، این تیکه کد داخل APIهای Node موقتا نگه‌داری میشه و تایمر ۲ثانیه‌ای توی اونجا براش شروع به شمارش میکنه. پس تا الان Call Stack خالیه و جا برای تسک‌های بعدی داریم.

سراغ console.log سومی میره( همزمان عکس کد رو هم ببینین). دوباره وارد APIهای Node میشه و تایمر 0ثانیه‌ای براش گذاشته میشه. و همچنان Call Stack خالی :)))

تایمر 0ثانیه برای console.log سومی تموم شد! اینجا نقش آخر داستان وارد میشه! Callback Queue?

مثلا Callback Queueعه?
مثلا Callback Queueعه?

لاگ سومیمون که تایمر 0ثانیه داشت، الان دیگه توی APIها نیست و این مورد زیر، وارد Callback Queue میشه:

Callback (0sec)

میره سراغ console.log آخری. تایمر نداره پس API و Callback Queue در کار نیست و فقط داخل Call Stack پوش، چاپ و سپس pop میشه. پس کنسولمون تا اینجای کار میشه:

'I can not wait!' / 'I am done!'

نکته جالب اینجاست. حتی main که اون اولین push هم بود pop میشه! و Call Stack خالی خالی میشه! حالا Callback Queue میبینه Call Stack خالیه و اینجاست که ابتدا (Callback (0sec و بعد از اون console.log سومی رو برای Call Stack میفرسته و به ترتیب push و pop میشن:

'I can not wait!' / 'I am done!' / 'Zero seconds'

همه این اتفاقا، زیر ۲ثانیه رخ داده! همشون توی زمان ۰ مطلق اتفاق افتاده. بعد از ۲ثانیه، از APIهای Node، لاگ دومی وارد Callback Queue میشه. پس مثل قبلی، همچین چیزی وارد Callback Queue میشه:

Callback (2sec)

دوباره Callback Queue میاد داخل Call Stack رو چک میکنه و میبینه خالیه. پس وقتشه که مثل قبلی اون دومورد (Callback (2sec و console.log رو push و سپس pop کنه.

یعنی خروجی‌ها به ترتیب اینجوری میشن(گزینه دوم تست بالا):

'I can not wait!' / 'I am done!' / 'Zero seconds' / 'Two seconds'

جمع‌بندی

یه جورایی میتونیم اینطور در نظر بگیریم که Callback Queue واسطه‌ایه بین APIهای Node و Call Stack. به این صورت که (Callback (nSec بعد از تموم‌شدن تایمر nثانیه، از طرف APIهای Node برای Callback Queue فرستاده میشه. توی Callback Queue چک میشه که آیا Call Stack خالی هست یا نه(حتی main هم نباید توی استک باشه). اگر خالی بود از صف dequeue میشه و داخل Call Stack پوش میشه.

صف صبحگاهی مدرسه رو در نظر بگیرین. بعد از اتمام مراسم صبحگاهی(تموم‌شدن تایمر nثانیه) و وقتی که دانش‌آموزا میخوان وارد کلاس بشن، ناظم مدرسه(Callback Queue) میاد کوتاهی ناخن دانش‌آموزاش(خالی بودن Call Stack) رو چک میکنه. اگه کوتاه(خالی) بود اجازه میده که وارد کلاس(Call Stack) بشن. اگه نه صبر میکنه تا تعهد بدن(Call Stack خالی بشه). کاملا واضح شد دیگه فکر کنم?

همین مورد که 2ثانیه و 0ثانیه طول کشید، میتونه یه request به API باشه. پس یادتون باشه که دقیقا همین اتفاقا میفته وقتی ما درخواستی رو ارسال میکنیم و منتظر response میمونیم.

کنجکاو باشیم!

دنبال همه چیز باشیم! هیچ یادگیری‌ای رو حتی برای کوتاه مدت، پشت گوش نندازیم. چون خیلی زود خودشو نشون میده و ممکنه اون تایم برای یادگیری خیلی دیر باشه.

همین مورد Call Stack ممکنه چیزی باشه که خیلیا بگن اون پشت داره اتفاق میفته و ما نمیبینیم پس به چه درد ما میخوره؟ در جواب باید گفت که همین ترتیب اجرای چند تیکه کد، میتونه باعث کشف یه باگ بزرگ توی پروژه بشه که هرکسی از پس اون برنمیاد.

آرزو میکنم شما اون فردی باشید که از پسش برمیاین?

موفق و سربلند باشید❤Reza Hasani