Skip to content

كيف تستطيع Nodejs تخديم آلاف الطلبات على Thread واحدة وما هو Event Loop؟

Published: at ٠٧:١٧ م

في هذه المقالة

المعالج ينفذ مهمة واحدة في آن واحد

ربما سمعت عن مفهوم المزامنة (Concurrency)، الذي يعني تنفيذ عدة مهام في وقت واحد.

على سبيل المثال: إنك تقرأ هذه المقالة على متصفحك، وقد تكون لديك ساعة تعمل في زاوية الشاشة وربما برامج أخرى قيد التشغيل في نفس الوقت.

في الواقع، نواة المعالج الواحدة يمكنها تنفيذ مهمة واحدة فقط في آن واحد. لكن ما يحدث هو أن النواة تقوم بالتبديل بين المهام بسرعة فائقة، مما يعطي انطباعًا بأنها تنفذ عدة مهام في وقت واحد. هذه العملية تسمى (Context Switching).

وصلنا اليوم إلى معالجاتٍ تحتوي على 32 نواة. هذا يعني أن المعالج يستطيع تنفيذ 32 مهمة في الوقت ذاته ذاته. لكن في حالة تطبيق ويب يستقبل آلاف الطلبات في الثانية، وعلى فرض أن كل طلب يستغرق ربع ثانية لتنفيذه. يمكن لكل نواة معالجة 4 طلبات في الثانية، مما يعني 128 طلبًا في الثانية فقط لمعالج ذو 32 نواة.

كيف إذاً يتعامل المعالج مع آلاف الطلبات في نفس الوقت ويقوم بتخديمها جميعاً وبسرعة عالية دون الانتظار لساعات؟

في الحقيقة كل برنامج على حاسوبك يُعتبر عملية (Process) تستطيع النواة التبديل بينها (Context Switching) بسرعة لتنفيذها جميعاً في نفس الوقت. فلو قمنا بتشغيل نفس برنامج الويب الخاص بنا 10 مرات على نفس النواة، فنظرياً سنستطيع تخديم ما يقارب 10 أضعاف الطلبات على النواة تماماً كما نقوم بتشغيل عدة برامج في نفس الوقت.

لكن هذه العملية (Context Switching) مكلفة للغاية وبطيئة إذ سيتوجب على حاسوبك تغيير مساحة الذاكرة التي يتعامل معها، وتحديث سجلات المعالج وعدة أمور أخرى. ولذلك تم اختراع نوع آخر من المهام وهي (Threads) وتكون أبناءً لعملية (Process) واحدة.

قد تحتوي كل (Process) على عدة مهام أصغر منها (Threads) وتتشارك هذه المهام الصغيرة في أمور أكثر مما يجعل (Context Switching) أقل تكلفة وبالتالي تنفيذ عدة مهام ضمن مهمة أب واحدة بشكل أسرع. على سبيل المثال، المتصفح الذي تقرأ منه هذه المقالة هو (Process)، وكل نافذة أو علامة تبويب تفتحها هي (Thread) ضمن هذه العملية. لكن حتى مع ذلك سيظل هناك (Context Switching) ولو كان أقل تكلفة من التبديل بين (Processes).

Nodejs تعمل على Thread واحدة

تم تصميم Nodejs لتعمل على Thread واحدة للتخلص من الوقت الضائع في عملية (Context Switching). لكن كيف لازالت تقوم بتخديم آلاف الطلبات في وقت واحد؟

أولاً. هناك نوعان من المهام؛ مهام تقوم بإشغال المعالج حتى تنتهي كالعمليات الحسابية، ومهام لا تقوم بإشغاله بشكل كامل كإرسال طلب Http مثلاً.

تسمى العمليات الأولى Blocking وتعني أن البرنامج لا يتابع التنفيذ حتى تنتهي العملية الحسابية الحالية (لأن المعالج مشغول)، أما الثانية تدعى (Non-Blocking) لأن المعالج يقوم بمعالجة إرسال طلب Http ثم هناك فترة لا يقوم فيها المعالج بشيء حتى تعود نتيجة الطلب وهذه الفترة هي ما تستغله Nodejs لتنفيذ مهام أخرى. وتقوم بهذه العملية عن طريق ما يدعى Event Loop وهو قلب Nodejs والمتحكم الأساسي فيها.


قبل الإكمال، لنأخذ مثالاً بسيطاً عن نوعي المهام التي تشغل المعالج (Blocking) والتي لا تشغله (Non-Blocking).

const TEN_SECONDS = 10_000;

function blockingTask() {
  const until = Date.now() + TEN_SECONDS;
  while (Date.now() < until) {
    // keep cpu busy
  }
}

function otherTask() {
  console.log("otherTask");
}

function main() {
  console.time("Time");
  blockingTask();
  console.timeEnd("Time");
  otherTask();
}

main();

إن خرج هذا المثال هو

Time: 9.999s
otherTask

وذلك لأن while تقوم بإشغال المعالج بشكل كامل لمدة ١٠ ثواني.

وأما هنا

function nonBlockingTask() {
  console.time("Fetch");
  fetch("https://www.google.com")
    .then(res => res.text())
    .then(() => {
      console.timeEnd("Fetch");
      console.log("Fetched google");
    });
}

function otherTask() {
  console.log("otherTask");
}

function main() {
  console.time("Time");
  nonBlockingTask();
  console.timeEnd("Time");
  otherTask();
}

main();

فالخرج سيكون

Time: 46.136ms
otherTask
Fetch: 376.619ms
Fetched google

لاحظ كيف تم تنفيذ المهمة الأخرى قبل أن تنتهي مهمة طلب Google.

ماذا عن القراءة والكتابة من و على الملفات؟

في مثال الكود أعلاه حول طلب Http. فإن العمليات على الشبكة لا تشغل المعالج (Non-Blocking) بطبيعتها (لأن هناك وقت انتظار ريثما تعود النتيجة). أما بالنسبة لعمليات قراءة الملفات أو الكتابة عليها، فتعتبر عادةً عمليات Blocking لأنها تشغل المعالج حتى تكتمل. ولكن كيف يمكن لـ Nodejs أن تقوم بقراءة وكتابة عدة ملفات في نفس الوقت؟

في الواقع، تعتمد Nodejs على مكتبة داخلية تدعى libuv لإدارة عمليات القراءة والكتابة من وعلى الملفات في نفس الوقت. عندما تحتاج Nodejs إلى قراءة أو كتابة ملف، يتم إرسال الطلب إلى libuv، التي ترسله بدورها إلى نظام التشغيل. يقوم نظام التشغيل بالقراءة أو الكتابة في (Process) مستقلة عن Nodejs، وعندما ينتهي من القراءة أو الكتابة، تقوم libuv بإرسال إشعار (Event) إلى Nodejs لتتمكن من متابعة العمل على البيانات التي تمت قراءتها أو كتابتها.

تقوم Nodejs باستغلال الوقت ريثما تعود النتيجة من libuv بتنفيذ مهام أخرى.

ما هو Event Loop ؟

هذا الجزء يعتبر قلب Javascript وهو ما يجعلها قادرة على تنفيذ عدة مهام في نفس الوقت، وهو الجزء الذي يقوم باستغلال وقت انتظار العمليات الـ (Non-Blocking) لتنفيذ مهام أخرى، وهو أيضاً من يقوم بتشغيل التوابع المرتجعة (Callbacks) لهذه العمليات عندما تعود بالنتيجة.

يوجد اختلاف بسيط بمراحل Event Loop على المتصفح و Event Loop على Nodejs لكن كليهما يعملان نفس العمل.

سنهتم في هذه المقالة بـ Nodejs و Event Loop الخاص بها وهو عبارة عن ٦ مراحل يتم تنفيذها بشكل تكراري لا متناهي مرحلةً تلو الأخرى، وهي:

١. مرحلة تنفيذ المؤقتات (Timers):

في هذه المرحلة يقوم Event Loop بتنفيذ التوابع (Functions) الموجودة ضمن setTimout و setInterval إن حان وقت تنفيذها.

٢. مرحلة تنفيذ التوابع المرتجعة المؤجلة (Pending Callbacks):

في هذه المرحلة يتم تنفيذ التوابع المرتجعة (Callbacks) المؤجل تنفيذها من حلقة Event Loop السابقة. غالباً ما تكون هذه التوابع متعلقة ب Nodejs داخلياً وليست من برنامج المستخدم.

٣. مرحلة Idle, Prepare:

يتم استخدامها داخلياً فقط من Nodejs لتنفيذ أمور لا أعلمها ولم أبحث بها.

٤. مرحلة تنفيذ التوابع المرتجعة المتعلقة بالدخل والخرج (I/O Callbacks):

وتسمى هذه المرحلة Poll وهي المرحلة الأهم في Event Loop حيث أنها تقرر إكمال تنفيذ البرنامج أو الانتظار لحين وصول توابع مرتجعة (Callbacks) جديدة. وتعتمد في هذا القرار على إن كان هناك توابع من setTimeout أو setInterval حان وقت تنفيذها. وعلى وجود توابع مرتجعة متلقة بالدخل والخرج I/O Callback تنتظر التنفيذ أو لا. وكذلك على وجود توابع من setImmediate أو لا.

فعند الدخول في هذه المرحلة:

٥. مرحلة تنفيذ توابع setImmediate:

تدعى هذه المرحلة Check وتقوم بتنفيذ التوابع التي تم استدعاؤها عن طريق setImmediate.

٦. مرحلة تنفيد التوابع المرتجعة المتعلقة بإغلاق المهام (Close Callbacks):

بعض المهام في Nodejs مثل Read/Write Streams تقوم عند غلقها بإرسال إشعار إغلاق (close” Event”).

يتم تنفيذ التوابع المرتجعة الخاصة بعمليات الإغلاق هذه في هذه المرحلة.

الملخص

تتجلى قوة Nodejs في قدرتها على تنفيذ عمليات الإدخال والإخراج (I/O) بسرعة فائقة وبشكل غير متزامن (Concurrent) بفضل Event Loop. لكنها في نفس الوقت ليست الخيار الأمثل لتنفيذ العمليات الحسابية المتزامنة التي تتطلب استهلاكًا مكثفًا للمعالج، مما قد يؤدي إلى منع Nodejs عن معالجة الطلبات أو المهام الأخرى، نظرًا لاعتمادها على Thread واحدة.

المراجع