آشنایی با مفاهیم Blocking و Non-Blocking در Node.js


در برخی زبان‌های سمت سرور همچون PHP اجرای اسکریپت‌ها اصطلاحاً به صورت Blocking انجام می‌شود و یکی از چیزهایی که Node.js را نسبت به زبان‌های سمت سروری از این دست متمایز می‌سازد، فیچری تحت عنوان Non-Blocking می‌باشد که در این مقاله قصد داریم بیشتر با ماهیت‌ این مفاهیم آشنا شویم (برای آشنایی با این محیط جاوااسکریپتی سمت سرور، می‌توانید به مقالهٔ Node.js چیست؟ مراجعه نمایید.)

I/O چیست؟
پیش از پرداخت به بحث اصلی، باید بدانیم که اساساً منظور از I/O چیست (این اصطلاح به صورت Eye-Oh تلفظ می‌شود.) به طور خلاص، I/O مخفف واژگان Input/Output است و به هرگونه تعامل سیستم با دنیای خارج اشاره دارد که این دنیای بیرون هم عبارت است از دیتای که به عنوان ورودی به سمت سیستم ارسال می‌شوند و یا دیتایی که سیستم آن‌ها را در پاسخ به یک درخواست از دیتابیس فراخوانی کرده و در اختیار سیستم (کاربر) قرار می‌دهد و CPU مرکزی است که تمامی این مسائل را هندل می‌کند.

I/O Port هم به آن دسته از پورت‌های سخت‌افزاری اطلاق می‌گردد که این امکان را در اختیار ما قرار می‌دهند تا بتوانیم دیوایس‌های ورودی همچون ماوس و کیبورد و همچنین دیوایس‌های خروجی همچون پرینتر یا مانیتور را به دستگاه خود وصل نماییم تا از آن طریق I/O امکان‌پذیر گردد (به منظور درک بهتر این موضوع، باید گفت که مثلاً ماوس یک دیوایس Input است زیرا فقط می‌‌تواند اقدام به ارسال دیتا کند و هرگز قادر به دریافت پاسخ از سمت سیستم نیست و یا مانیتور یک دیوایس Output است که فقط می‌تواند اقدام به نمایش دیتا کند و امکان اینکه ریسپانسی به سمت سیستم ارسال کند برایش در نظر گرفته نشده است اما برخی دیوایس‌ها هستند که هر دو قابلیت برایشان در نظر گرفته شده که از آن جمله می‌توان به هارددیسک اشاره کرد.) از جمله دیگر دیوایس‌های I/O نیز می‌توان به موارد زیر اشاره کرد:

- سی‌دی درایو
- هارددیسک
- مودم
- کارت شبکه
- کارت صدا

آشنایی با مفهوم Blocking
چنانچه یک زبان برنامه‌نویسی همچون PHP را مد نظر قرار دهیم، Blocking به این مسئله اشاره دارد که اجرای اسکریپت‌ها بدین صورت عملی می‌گردد که یک پروسه باید تکمیل گردد و پس از آنکه سیستم آزاد شد، پروسه (اسکریپت) بعدی امکان اجرا پیدا می‌کند. به طور مثال داریم:

echo time();
echo 'Hello World';

در اسکریپت فوق، ابتدا مقدار بازگشتی تابع ()time نمایش داده می‌شود سپس دستور echo دوم که این وظیفه را دارا است تا اِسترینگ Hello World را چاپ کند، اجرا خواهد شد.

به طور خلاصه، گفته می‌شود که متدهای Blocking به صورت اصطلاحاً Synchronous اجرا می‌شوند در صورتی که متدهای Non-Blocking به صورت Asynchronous اجرا می‌شوند و نیاز به توضیح هم نیست که صفت Synchronous و دارای معانی مختلفی همچون «هم‌گام»، «هم‌زمان» و «هم‌وقت» است و Asynchronous هم معانی مختلفی مثل «ناهم‌گام» یا «غیرهم‌زمان» دارد و این در صورتی است که می‌توان اصطلاحات (Blocking و Synchronous) و همچنین (Non-Blocking و Asynchronous) را مترادف یکدیگر در نظر گرفت اما این در حالی است که چنانچه معنی فارسی این صفات را در ذهن خود متصور شویم، بیش از آنکه این مطلب روشن شود، گمراه خواهیم شد! به عبارتی، اگر بگوییم فلان متد از جنس Blocking به صورت هم‌زمان (Synchronous) اجرا می‌شود، ممکن است این ایماژ ایجاد گردد که چگونه می‌شود وقتی می‌گوییم متدی از جنس Blocking که کل اسکریپت را متوقف می‌سازد تا تکمیل گردد سپس ادامهٔ کد اجرا می‌شود، متدی هم‌زمان است؟

اساساً بوجود آمدن چنین علامت سؤالی در ذهن شما درست است زیرا معادلی همچون «هم‌زمان» اصلاً حق مطلب را ادا نمی‌کند و بهتر است همان معادل لاتین Synchronous را به کار بریم. در عین حال، برای روشن‌تر شدن این مطلب می‌توان آنچه تاکنون گفته شد را این‌گونه تفسیر کرد که یک متد Blocking یا Synchronous، که هر دو هم‌معنی هستند، جلوی اجرای الباقی اسکریپت را می‌گیرد اما یک متد Non-Blocking یا Asynchronous این اجازه را به اپلیکیشن می‌دهد تا در آنِ واحد پردازش موازی تَسک دیگری نیز صورت گیرد.

به طور کلی، در این آموزش هر جایی که از واژهٔ Blocking یا Synchronous استفاده شد منظور یک چیز است و بر همین منوال هر زمانی که از واژگان Non-Blocking و Asynchronous استفاده شد نیز منظور مفهوم یکسانی بوده است.

مقایسهٔ پردازش Blocking و Non-Blocking در Node.js
برای درک بهتر این موضوع، اسکریپت زیر را در نظر می‌گیریم که به صورت Blocking اجرا می‌گردد:

const fs = require('fs');
const data = fs.readFileSync('/file.md');

در مثال فوق، از ماژول File System نودجی‌اس تحت عنوان fs استفاده کرده‌ایم به طوری که در خط دوم با استفاده از فانکشن ()readFileSync شروع به خواندن محتوای فایلی تحت عنوان file.md کرده‌ایم. آنچه در مورد این بلوک کد وجود دارد این است که اجرای این اسکریپت به صورت Blocking است بدین صورت که تا زمانی که فایل file.md به صورت کامل خوانده نشود، هیچ‌گونه کد جاوااسکریپت دیگری اجرا نخواهد شد و این در حالی است که اگر در این بین با اروری مواجه شویم، برنامه‌ٔ احتمالاً کِرَش خواهد کرد. حال در ادامه قصد داریم ببینیم که نسخهٔ Non-Blocking کدهای فوق به چه صورت خواهد بود:

const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});

فانکشنی تحت عنوان ()readFile داریم که نسخهٔ Non-Blocking فانکشن ()readFileSync است و به طور خلاصه این مزیت را دارا است تا دولوپر بتواند در صورت نیاز اسکریپت دیگری را نیز اجرا کند و سیستم هرگز منتظر نخواهد ماند تا خواندن فایل file.md تکمیل گردد (اگر در این بین اِکسپشنی صورت گیرد، آن اِکسپشن در متغیر err ذخیره شده و با یک دستور شرطی می‌توانیم آن را نمایش دهیم و چنانچه همه چیز موفقیت‌آمیز باشد، نتیجه در متغیر data ذخیره خواهد شد که بعداً خواهیم دید به چه شکل می‌توان آن را نمایش داد.) برای روشن‌تر شدن این تفاوت، مجدد به اسکریپت اول بازمی‌گردیم که به صورت Blocking نوشته شده است:

const fs = require('fs');
const data = fs.readFileSync('/file.md');
console.log(data);
moreWork(); // will run after console.log

نسخهٔ Non-Blocking یا Asynchronous کدهای جدید هم به صورت زیر است:

const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork(); // will run before console.log

در تفسیر هر دو بلوک فوق باید گفت که در مثال اول (console.log(data قبل از فانکشن فرضی ()moreWork اجرا می‌گردد چرا که کدها به موازات یکدیگر نوشته شده‌اند و از بالا به پایین اجرا می‌شوند و اجرای فانکشن ()moreWork مستلزم این است که ابتدا فانکشن ()readFileSync به درستی کارش را انجام داده باشد سپس (console.log(data اجرا شود و در نهایت نوبت به ()moreWork می‌رسد؛ اما در مثال دوم که از جنس Asynchronous است که این امکان را به ما می‌دهد تا در آنِ واحد دو تَسک مختلف اجرا شود، فانکشن ()readFile به صورت Non-Blocking اجرا می‌شود به طوری که امکان اجرای پروسه‌های دیگری را هم داریم و از همین روی جاوااسکریپت می‌تواند اگر خواندن فایل file.md طولانی شد و یا در حین خواندن این فایل اِکسپسنی رخ داد، به سراغ اجرای فانکشن ()moreWork برود و همین فیچر در عین حال ساده منجر شده تا Node.js برای اپلیکیشن‌هایی نیاز به I/O فراوان از یکسو و همچنین سرعت بالای اجرای درخواست‌های کاربران از سوی دیگر دارد به گزینهٔ ایده‌آلی مبدل گردد.

برای درک بهتر این موضوع، سناریویی فرضی را مد نظر قرار می‌دهیم. فرض کنیم هر ریکوئستی که کاربر به سمت سرور ارسال می‌کند، چیزی در حدود ۵۰ میلی‌ثانیه به طول خواهد انجامید تا تکمیل گردد و از این مقدار ۴۵ میلی‌ثانیه مربوط به فراخوانی و یا ثبت دیتا روی دیتابیس است که به شکلی Non-Blocking یا Asynchronous هم می‌توان این تَسک‌ها را انجام داد که در چنین شرایطی، استفاده از این رویکرد مقدار زمان ۴۵ میلی‌ثانیه‌ای را به ازای هر ریکوئست آزاد می‌سازد تا بتوان به عملی کردن ریکوئست‌های دیگر پرداخت و این یعنی ایجاد یک تجربهٔ کاربری فوق‌العاده خوب برای کاربرانی که دغدغهٔ سرعت بالا دارند.

آیا ادغام متدهای Blocking و Non-Blocking کار درستی است؟
پاسخ به این پرسش هم آری است و هم خیر زیرا اگر این کار به درستی صورت نگیرد، منجر به ایجاد اختلال در اپلیکیشن می‌شود. برای درک بهتر این موضوع، مثال فوق به صورت زیر تکمیل می‌کنیم:

const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
fs.unlinkSync('/file.md');

همان‌طور که مشاهده می‌شود، از یک متد Non-Blocking تحت عنوان ()readFile و یک متد Blocking به نام ()unlinkSync استفاده کرده‌ایم و نیاز به توضیح نیست که اگر خواندن فایل file.md بیش از حد طولانی شود، مفسر جاوااسکریپت به سراغ خط ششم می‌رود و در حالی که فایل file.md دارد توسط سیستم پردازش می‌شود، فانکشن ()unlinkSync این فایل را حذف می‌‌کند! برای رفع این مشکل، می‌توان کدهای فوق را به صورت زیر ریفکتور کرد:

const fs = require('fs');
fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr;
  console.log(data);
  fs.unlink('/file.md', (unlinkErr) => {
    if (unlinkErr) throw unlinkErr;
  });
});

در این مثال، اولاً اینکه از فانکشی از جنس Non-Blocking تحت عنوان ()unlink استفاده کرده‌ایم که در لایبرری استاندارد نودجی‌اس وجود دارد؛ ثانیاً اینکه این فانکشن را داخل Callback (پاسخ) فانکشن ()readFile قرار داده‌ایم که این تضمین را ایجاد می‌کند که قبل از آنکه فایل به صورت کامل خوانده نشده، هرگز این فایل حذف نگردد.



بهزاد مرادی