برنامه‌نویسی async در جاوااسکریپت (قسمت اول)

یکی از نقدهایی که معمولا به جاوااسکریپت وارد می‌شود این است که بعضی مکانیزم‌های پایه‌ای آن (مثل coercion)، ما را مجبور به یادگیری نکات و استثناهایی می‌کند که لزوما در زبان‌های دیگر قابل استفاده نیستند و بیشتر داخل خود جاوااسکریپت کاربرد دارند.
اما با عمیق شدن در مفاهیم برنامه‌نویسی async در جاوااسکریپت، با دنیایی از مفاهیم و پترن‌های جذاب رو به رو می‌شویم که یادگیری آن‌ها، ما را به برنامه‌نویس بهتری تبدیل می‌کند!

در این سری مقاله سعی می‌کنیم این مفاهیم مثل event loop, callback, promise و ... را عمیق‌تر یاد بگیریم. امیدوارم برای شما هم مفید باشد و اگر نکته‌ای در مطالب جا افتاده بود، در قسمت نظرات بنویسید.

قبل از هر چیز بیایید با مفاهیم مرتبط با برنامه‌نویسی async آشنا شویم:

Blocking/non-Blocking IO

تقریبا تمام برنامه‌هایی که با جاوااسکریپت نوشته می‌شوند، به نحوی با عملیات IO، مثل فرستادن request به سرور، هندل کردن input کاربر و ... سروکار دارند.
همچنین می‌دانیم که این عملیات IO، معمولا نسبت به سرعت پردازش CPU، بسیار کند هستند و به زمان بیشتری برای کامل شدن احتیاج دارند (IO Bound).
دو روش کلی برای انجام دادن IO وجود دارد:

Blocking IO

در این روش، وقتی یک request به سرور می‌فرستیم، (یا هر عملیات IO دیگر) اجرای برنامه متوقف می‌شود و تا زمانی که سرور به request پاسخ ندهد، threadی که کد روی آن اجرا می‌شود، block می‌شود.

در بعضی زبان‌ها برای اینکه بتوانیم از blocking IO استفاده کنیم و در عین حال کل برنامه freeze نشود، می‌توانیم برای هر عملیات IO، یک thread جدید ایجاد کنیم، تا بقیه‌ی پردازش‌های برنامه در یک thread مجزا ادامه پیدا کند. اما با توجه به اینکه جاوااسکریپت فقط بر روی یک thread اجرا می‌شود، امکان استفاده از این روش داخل جاوااسکریپت را نداریم.
در واقع استفاده از blocking IO در یک سیستم single-thread (مثل برنامه‌های جاوااسکریپتی) فاجعه به بار می‌آورد! چرا که با هر عملیات IO، کل برنامه به کلی متوقف می‌شود و کاربر نمی‌تواند با برنامه تعامل داشته باشد، تا زمانی که عملیات IO تکمیل شود.

Blocking IO
Blocking IO


Non-Blocking IO

در این روش، بعد از فرستادن request به سرور، صبر نمی‌کنیم تا request تکمیل شود و CPU بلافاصله می‌تواند بقیه پردازش‌های برنامه را انجام دهد.

Non Blocking IO
Non Blocking IO

همانطور که می‌بینید، در این کد با ارسال request به سرور، بلافاصله اجرای برنامه را ادامه می‌دهیم و هر وقت نتیجه از سمت سرور برگشت، callbackی که تعریف کرده‌ایم صدا می‌شود (در مقاله‌های بعدی معایب callbackها را بررسی خواهیم کرد!).
به این ترتیب، یک قسمت از برنامه الان اجرا می‌شود و بقیه قسمت‌های برنامه، بعدتر و وقتی request از سرور برمی‌گردد اجرا می‌شود.
به این عملیات که الان اجرا نمی‌شوند و اجرای برنامه را از نظر زمانی تقسیم می‌کنند، asynchronous می‌گوییم.
(دقت کنید که non-blocking IO فقط یک مثال از asynchrony است. و برای انجام کارهای دیگر، مثل نمایش انیمیشن‌ها و ... هم از برنامه نویسی async استفاده می‌کنیم.)

Concurrency در مقابل parallelism!

اما چطور جاوااسکریپت با وجود single-thread بودن می‌تواند “هم‌زمان” با ارسال request و ... کارهای دیگر را هم انجام دهد؟ در واقع اگر به برنامه‌هایی که با جاوااسکریپت نوشته می‌شوند دقت کنید، به نظر می‌رسد که چند کار مختلف به صورت هم‌زمان انجام می‌شود. به عنوان مثال هم‌زمان با scroll شدن صفحه توسط کاربر، یک انیمیشن اجرا می‌شود و ...
چطور همه‌ی این کارها فقط بر روی یک thread انجام می‌شوند؟!
برای جواب به این سوال باید با دو مفهوم مهم concurrency و parallelism آشنا باشیم.

Parallelism

در parallelism، همانطور که از اسم آن مشخص است، چند تسک، به صورت موازی و دقیقا در یک لحظه پردازش می‌شوند. برای پردازش parallel، معمولا از چند thread (یا process) برای اجرای عملیات مختلف استفاده می‌شود. بنابراین با توجه به single-thread بودن جاوااسکریپت، عملا نمی‌توانیم دو کار را دقیقا در یک لحظه انجام دهیم.

Concurrency

همچنین Concurrency به این معنی است که چند تسک مختلف در یک بازه‌ی زمانی انجام شوند (اما نه لزوما هم‌زمان و در یک لحظه). در واقع concurrency مفهوم کلی‌تری نسبت به parallelism است و می‌توانیم بدون پردازش parallel هم، concurrency داشته باشیم.

به عنوان مثال فرض کنید یک پروژه داریم که بخش‌های مختلفی (مثلا فرانت-اند و بک-اند) دارد.
اگر چند تیم بر روی بخش‌های مختلف این پروژه کار کنند، بخش‌های مختلف پروژه به صورت هم‌زمان و parallel پیش‌رفت می‌کند.
اما اگر فقط یک نفر روی این پروژه کار کند، در یک لحظه، فقط بر روی یک بخش از پروژه کار می‌شود و بخش‌های مختلف به صورت هم‌زمان پیش‌رفت نمی‌کنند. اما هم‌چنان پروژه به صورت concurrent انجام می‌شود. چرا که بخش‌های مختلف پروژه در یک بازه‌ی زمانی با هم پیش‌رفت می‌کنند. هر چند دقیقا در یک لحظه، بر روی همه‌ی بخش‌ها کار نمی‌شود.

Parallel Concurrency vs Non-Parallel Concurrency
Parallel Concurrency vs Non-Parallel Concurrency


در نتیجه با اینکه جاوااسکریپت به صورت parallel اجرا نمی‌شود، اما می‌توانیم بیش از یک کار را در یک بازه‌ی زمانی به صورت concurrent انجام دهیم.
به این‌صورت که انجین جاوااسکریپت در هر لحظه فقط یک کار را انجام می‌دهد و به عنوان مثال اگر در طول پردازش responseی که از سرور برگشته است، کاربر روی یک دکمه کلیک کند، باید تا پایان پردازش response صبر کنیم تا کلیک کاربر پردازش شود. به این ترتیب در یک بازه‌ی زمانی چند کار مختلف پردازش می‌شوند.

Multitasking

حالا که متوجه شدیم فقط با استفاده از یک thread هم می‌توانیم بیش از یک کار را در یک بازه‌ی زمانی انجام دهیم، بیایید نگاهی به دو روش کلی multitasking در نرم‌افزار داشته باشیم. و در نهایت ببینیم کارکرد جاوااسکریپت به کدام یکی از این دو روش نزدیک‌تر است.

Preemptive Multitasking

در این روش معمولا برای پردازش تسک‌های مختلف، چند thread (یا process) ایجاد می‌شود و اگر اجرای یک thread بیش از حد زمان بگیرد، سیستم عامل می‌تواند اجرای آن را متوقف کند.
در کل این threadها توسط سیستم عامل مدیریت می‌شوند و حتی ممکن است به صورت موازی (parallel) اجرا شوند.

Cooperative Multitasking

در این روش، کل برنامه از دید سیستم عامل فقط بر روی یک thread اجرا می‌شود.
اما تسک‌های مختلف داخل خود برنامه مدیریت می‌شوند. به این صورت که در هر لحظه یک تسک اجرا می‌شود و مقداری پردازش روی CPU انجام می‌دهد. سپس CPU را آزاد می‌کند (اصطلاحا yield می‌کند) تا تسک بعدی اجرا شود. به این ترتیب تسک‌های مختلف یک thread را به صورت اشتراکی استفاده می‌کنند.
به این روش cooperative گفته می‌شود، چون برای کارکرد درست کل سیستم، همه‌ی تسک‌ها باید با هم هم‌کاری کنند. اگر یک تسک پردازش طولانی روی CPU انجام دهد، کل برنامه کند می‌شود.
در نتیجه در این روش مدیریت تسک‌های مختلف بر عهده‌ی برنامه‌نویس است.

احتمالا متوجه شدید که عملکرد جاوااسکریپت به شدت شبیه به مدل cooperative multitasking است!
یعنی در جاوااسکریپت هم، تسک‌های concurrent به صورت اشتراکی از یک thread استفاده می‌کنند و ما به عنوان برنامه‌نویس باید این تسک‌ها را به درستی مدیریت کنیم. اگر یک تابع، پردازش طولانی روی CPU انجام می‌دهد، باید آن را به چند تسک کوچک‌تر تقسیم کنیم تا کل برنامه کند نشود و ...

تقریبا تمام مطالب بعدی این سری مقاله در راستای مدیریت مناسب این تسک‌های concurrent از نظر خوانایی کد، عمل‌کرد و ... خواهد بود.

خلاصه

در این مقاله با مفاهیم مرتبط با برنامه‌نویسی async مثل concurrency و انواع multitasking آشنا شدیم و نحوه‌ی مدیریت تسک‌های asynchronous در جاوااسکریپت را دیدیم.

اما از هر جنبه که کارکرد جاوااسکریپت را بررسی کردیم، به یک نتیجه‌ی واحد رسیدیم:
“جاوااسکریپت چندین تسک را در یک بازه‌ی زمانی بر روی یک thread پردازش می‌کند. و در هر لحظه فقط یک تسک را انجام می‌دهد.”

در قسمت بعدی با Event Loop آشنا می‌شویم و نمود عملی‌تر این مفاهیم را می‌بینیم.