ستكون Infa مفيدة لمطوري JS الذين يرغبون في فهم جوهر العمل بعمق مع Node.js و Event Loop. يمكنك التحكم بوعي وأكثر مرونة في تدفق البرنامج (خادم الويب).
جمعت هذه المقالة بناءً على تقريري الأخير للزملاء.
في نهاية المقال ، توجد مواد مفيدة للدراسة المستقلة.
كيف هي Node.js. ميزات غير متزامن
دعونا نلقي نظرة على هذا الكود: إنه يوضح تمامًا تزامن تنفيذ التعليمات البرمجية في Node.js. يتم تقديم طلب في مكان ما على GitHub ، ثم يتم قراءة ملف ويتم عرض النتيجة في وحدة التحكم. ما هو واضح من هذا الرمز متزامن؟

افترض أن هذا هو خادم ويب تجريدي يقوم بإجراء عمليات على جهاز توجيه. إذا وصل طلب وارد على هذا الموجه ، فإننا نقدم طلبًا إضافيًا ونقرأ الملف ونطبعه على وحدة التحكم. وفقًا لذلك ، فإن الوقت الذي يقضيه في طلب وقراءة ملف ، سيتم حظر الخادم ، ولن يكون قادرًا على معالجة أي طلبات واردة أخرى ، ولن يقوم بإجراء عمليات أخرى.
ما هي الخيارات لحل هذه المشكلة؟
- خاصية تعدد
- عدم حظر الإدخال / الإخراج
بالنسبة للخيار الأول (تعدد العمليات) ، يوجد مثال جيد على خادم الويب Apache vs Nginx.

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

يظهر بوضوح أن المدخلات / المخرجات غير المحجوبة تكون أكثر كفاءة.
تم إجراء إدخال / إخراج بدون حظر بفضل أنظمة التشغيل الحديثة التي توفر هذه الآلية - أداة إلغاء تعدد الأحداث.
أداة إلغاء التعددية هي آلية تتلقى طلبًا من أحد التطبيقات وتسجيله وتنفيذه.

في الجزء العلوي من الرسم البياني ، نرى أن لدينا تطبيقًا وأن العمليات تتم فيه (دعنا نقرأ ملفًا). للقيام بذلك ، يتم تقديم طلب إلى demultiplexer الحدث ، يتم إرسال مورد هنا (رابط إلى الملف) ، العملية المطلوبة و رد الاتصال. الحدث demultiplexer يسجل هذا الطلب وإرجاع السيطرة مباشرة إلى التطبيق - وبالتالي ، لا يتم حظره. ثم ينفذ عمليات على الملف ، وبعد ذلك ، عند قراءة الملف ، يتم تسجيل رد الاتصال في قائمة انتظار التنفيذ. ثم "حلقة الأحداث" بشكل متزامن بمعالجة كل رد الاتصال من قائمة الانتظار هذه. وفقًا لذلك ، تُرجع النتيجة إلى التطبيق. مزيد (إذا لزم الأمر) كل شيء يتم مرة أخرى.
وبالتالي ، بفضل I / O غير المحظورة ، يمكن أن يكون Node.js غير متزامن.
سأوضح أنه في هذه الحالة ، فإن نظام التشغيل هو الذي يوفر لنا مدخلات / مخرجات غير محظورة. إلى حظر الإدخال / الإخراج (بشكل عام ، من حيث المبدأ ، إلى عمليات الإدخال / الإخراج) ، نقوم بتضمين طلبات الشبكة والعمل مع الملفات.
هذا هو المفهوم العام لعدم حظر الإدخال / الإخراج. عندما ظهرت الفرصة ، استلهم Ryan Dahl ، وهو مطور Node.js ، من تجربة Nginx ، التي استخدمت I / O غير المحظورة ، وقررت إنشاء منصة خاصة للمطورين. أول شيء كان بحاجة إلى القيام به هو "تكوين صداقات" على منصته باستخدام أداة إلغاء تعدد الأحداث. كانت المشكلة هي أن demultiplexer تم تنفيذه بشكل مختلف في كل نظام تشغيل ، وكان عليه أن يكتب مجموعة ، والتي أصبحت تعرف فيما بعد باسم libuv. هذه مكتبة مكتوبة بلغة C. وهي توفر واجهة واحدة للعمل مع مزعج الأحداث.
ميزات مكتبة Libuv

في Linux ، من حيث المبدأ ، في الوقت الحالي ، يتم حظر جميع العمليات مع الملفات المحلية. بمعنى ، يبدو أن هناك إدخال / إخراج غير محظور ، لكن عند العمل مع الملفات المحلية لا تزال العملية تحظره. هذا هو السبب في أن libuv يستخدم مؤشرات الترابط داخليًا لمضاهاة الإدخال / الإخراج غير المحظور. تظهر 4 سلاسل من الخانات ، وهنا نحتاج إلى تحقيق الاستنتاج الأكثر أهمية: إذا أجرينا حوالي 4 عمليات ثقيلة على الملفات المحلية ، فسنحظر تطبيقنا بالكامل (إنه موجود على Linux ، ولا تفعل أنظمة التشغيل الأخرى).

في هذه الشريحة ، نرى بنية Node.js. للتفاعل مع نظام التشغيل ، يتم استخدام مكتبة libuv المكتوبة باللغة C ؛ لتجميع شفرة JavaScript إلى رمز الجهاز ، يتم استخدام محرك Google V8 ، وهناك أيضًا مكتبة Node.js Core ، والتي تحتوي على وحدات للعمل مع طلبات الشبكة ونظام الملفات ووحدة نمطية للتسجيل. أن كل هذا تفاعل مع بعضها البعض ، تتم كتابة Node.js Bindings. هذه المكونات 4 تشكل بنية Node.js. آلية حلقة الحدث نفسها في libuv.
حلقة الحدث

هذا هو أبسط تمثيل لما يشبه Event Loop. هناك قائمة انتظار معينة للأحداث ، وهناك دورة لا نهاية لها من الأحداث التي تنفذ عمليات من قائمة الانتظار بشكل متزامن ، وتقوم بتوزيعها بشكل أكبر.
توضح هذه الشريحة كيف تبدو "حلقة الأحداث" مباشرةً في Node.js.

هناك ، تنفيذ أكثر إثارة للاهتمام وأكثر تعقيدا. في الأساس ، حلقة الأحداث هي حلقة حدث ، وهي لا نهائية طالما يوجد شيء ما يجب القيام به. يتم تقسيم "حلقة الأحداث" في Node.js إلى عدة مراحل. (يجب مقارنة المراحل من الشريحة 8 بالكود المصدري في الشريحة 9.)

المرحلة 1 - الموقتات
يتم تنفيذ هذه المرحلة مباشرة بواسطة Event Loop. (مقتطف الشفرة باستخدام uv_update_time) - هنا يتم تحديث ببساطة وقت بدء حلقة العمل.
uv_run_timers - في هذه الطريقة ، يتم تنفيذ الإجراء المؤقت التالي. هناك مجموعة معينة ، وبشكل أكثر دقة ، مجموعة من أجهزة ضبط الوقت ، وهذا هو نفس قائمة الانتظار التي توجد فيها أجهزة ضبط الوقت. يتم استخدام جهاز ضبط الوقت مع أصغر وقت ، مقارنةً بالوقت الحالي من "حلقة الأحداث" ، وإذا حان الوقت لتنفيذ هذا المؤقت ، يتم تنفيذ رد الاتصال به. تجدر الإشارة هنا إلى أن Node.js لديه تطبيق setTimeout وهناك setInterval. بالنسبة إلى libuv ، هذا هو نفس الشيء بشكل أساسي ، فقط setInterval لا يزال يحتوي على علامة تكرار.
وفقًا لذلك ، إذا كان هذا المؤقت يحتوي على علامة تكرار ، فسيتم وضعه مرة أخرى في قائمة انتظار الأحداث ثم معالجته بنفس الطريقة.
المرحلة 2 - عمليات الاسترجاعات I / O
نحن هنا بحاجة إلى العودة إلى الرسم البياني حول عدم حظر الإدخال / الإخراج.
عندما يقرأ demultiplexer الحدث ملفًا ويصنّف رد الاتصال ، فإنه يتوافق فقط مع مرحلة رد الاتصال I / O. يتم إجراء عمليات الاسترجاعات هنا من أجل عدم حظر الإدخال / الإخراج ، أي أنها بالضبط الوظائف التي يتم استخدامها بعد طلب قاعدة بيانات أو مورد آخر أو لقراءة / كتابة ملف. يتم تنفيذها بدقة في هذه المرحلة.
في الشريحة 9 ، يبدأ تنفيذ وظيفة رد الاتصال I / O في السطر 367: ran_pending = uv_run_pending (loop).
3 المرحلة - الانتظار والتحضير
هذه عمليات داخلية لعمليات الاسترجاعات ، في الواقع ، لا يمكننا التأثير على المرحلة ، بشكل غير مباشر فقط. هناك عملية.التالي ، قد يتم تنفيذ رد الاتصال به عن غير قصد في مرحلة الإعداد والإعداد. يتم تنفيذ process.nextTick في المرحلة الحالية ، أي في الواقع ، يمكن أن تعمل process.nextTick في أي مرحلة تمامًا. لا توجد أداة جاهزة لتشغيل الكود في مرحلة "انتظار ، تحضير" في Node.js.
في الشريحة 9 ، تتوافق الأسطر 368 ، 369 مع هذه المرحلة:
uv_run_idle (حلقة) - الانتظار ؛
uv_run_prepare (حلقة) - التحضير.
4 المرحلة - المسح
هذا هو المكان الذي يتم فيه تنفيذ جميع التعليمات البرمجية التي نكتبها في JS. في البداية ، جميع الطلبات التي نقدمها تصل إلى هنا ، وهذا هو المكان الذي يمكن فيه حظر Node.js. إذا وصلت أي عملية حسابية ثقيلة إلى هنا ، في هذه المرحلة ، قد يتجمد تطبيقنا فقط وينتظر حتى تكتمل هذه العملية.
في الشريحة 9 ، تكون وظيفة الاستقصاء على السطر 370: uv_io_poll (حلقة ، مهلة).
5 المرحلة - تحقق
يوجد جهاز ضبط وقت setImmediate في Node.js ، ويتم تنفيذ عمليات الاسترجاعات الخاصة به في هذه المرحلة.
في التعليمات البرمجية المصدر ، هذا السطر 371: uv_run_check (حلقة).
6 المرحلة (الأخيرة) - أحداث رد الاتصال قريبة
على سبيل المثال ، يحتاج مقبس الويب إلى إغلاق الاتصال ، في هذه المرحلة سيتم استدعاء رد اتصال لهذا الحدث.
في التعليمات البرمجية المصدر ، هذا السطر 372: uv_run_closing_handless (loop).
وفي النهاية ، Event Loop Node.js كما يلي

أولاً ، في قائمة انتظار المؤقت ، يتم تنفيذ المؤقت ، وقد اقتربت الفترة.
ثم يتم تنفيذ عمليات الاسترداد I / O.
ثم الكود هو الأساس ، ثم setImmediate والأحداث وثيق.
بعد ذلك ، كل شيء يتكرر في دائرة. لإثبات ذلك ، سأفتح الكود. كيف سيتم تنفيذها؟

ليس لدينا أجهزة توقيت في الخط ، وبالتالي فإن حلقة الأحداث تتحرك. لا توجد عمليات استدعاء I / O-call ، لذلك نذهب على الفور إلى مرحلة الاقتراع. كل التعليمات البرمجية الموجودة هنا يتم تنفيذها مبدئيًا في مرحلة الاقتراع. لذلك ، أولاً نقوم بطباعة script_start ، يتم وضع setInterval في قائمة انتظار المؤقت (لم يتم التنفيذ ، فقط وضع). يتم وضع setTimeout أيضًا في قائمة انتظار المؤقت ، ثم يتم تنفيذ الوعود: الوعد الأول 1 ثم الوعد 2.
في علامة التجزئة التالية (حلقة الأحداث) ، نعود إلى مرحلة المؤقت ، وهنا في قائمة الانتظار يوجد بالفعل مؤقتان: setInterval و setTimeout. كلاهما تأخر 0 ، على التوالي ، وهما على استعداد للتنفيذ.
يتم تنفيذ SetInterval (الإخراج إلى وحدة التحكم) ، ثم setTimeout 1. لا توجد عمليات رد اتصال I / O غير محظورة ، ثم ستكون هناك مرحلة اقتراع ، وسيتم عرض الوعد 3 والوعد 4 في وحدة التحكم.
بعد ذلك ، يتم تسجيل مؤقت setTimeout. هذا ينتهي القراد ، انتقل إلى القراد التالي. هناك أجهزة ضبط الوقت مرة أخرى ، والإخراج إلى وحدة التحكم هو setInterval و setTimeout 2 ، ثم يتم عرض الوعد 5 والوعد 6.
لقد قمنا بمراجعة Event Loop ويمكننا الآن التحدث بمزيد من التفاصيل حول تعدد العمليات.
خيوط - worker_threads وحدة
لقد ظهر مؤشر الترابط في Node.js بفضل الوحدة النمطية worker_threads في الإصدار 10.5. وفي الإصدار العاشر ، تم إطلاقه حصريًا باستخدام مفتاح العامل التجريبي ، ومن الإصدار الحادي عشر كان من الممكن البدء بدونه.
يحتوي Node.js أيضًا على وحدة نمطية للكتلة ، ولكنها لا ترفع مؤشرات الترابط - فهي تثير العديد من العمليات. قابلية تطبيق هو هدفه الأساسي.

كيف تبدو عملية واحدة:
1 عملية Node.js ، مؤشر ترابط واحد ، حلقة حدث واحدة ، محرك V8 و libuv.
إذا بدأنا X خيوط ، ثم يبدو كما يلي:
1 عملية Node.js ، مؤشرات ترابط X ، حلقات أحداث X ، محركات X V8 و X libuv.
تخطيطيا ، يبدو على النحو التالي

لنأخذ مثالا.

أبسط خادم الويب على اكسبرس. هناك 2 طرق - / و / الدهون العملية.
هناك أيضا دالة generRandomArr (). انها تملأ مجموعة مع مليوني السجلات وفرزها. لنبدأ الخادم.
نحن نطلب / عملية الدهون. وفي الوقت الذي يتم فيه تنفيذ عملية فرز المصفوفة ، نرسل طلبًا آخر للتوجيه / ، ولكن للحصول على الإجابة ، يتعين علينا الانتظار حتى يتم فرز المصفوفة. هذا هو تنفيذ موضوع واحد الكلاسيكية. نحن الآن نربط وحدة worker_threads.

نقدم طلبًا إلى / fat-operation ومن ثم - إلى / ، من خلالها نحصل على الإجابة فورًا - Hello world!
بالنسبة لعملية فرز المصفوفة ، قمنا برفع سلسلة رسائل منفصلة لها مثيلها الخاص بـ Event Loop ، ولا تؤثر على تنفيذ الكود في الخيط الرئيسي.
سيتم "تدمير" مؤشر ترابط عندما لا يكون لديه عمليات لأداء.
نحن ننظر إلى شفرة المصدر. نقوم بتسجيل العامل على السطر 26 ، وإذا لزم الأمر ، نقوم بتمرير البيانات إليه. في هذه الحالة ، أنا لا أحيل أي شيء. ثم نشترك في الأحداث: خطأ ورسالة. في العامل ، تسمى الوظيفة ، يتم فرز مجموعة من مليوني سجل. بمجرد فرزها ، نرسل النتيجة إلى الدفق الرئيسي جيدًا من خلال post_message.

في الموضوع الرئيسي ، نلاحظ هذه الرسالة ونرسل النتيجة إلى النهاية. العامل والخيط الرئيسي لديهم ذاكرة مشتركة ، لذلك لدينا الوصول إلى المتغيرات العالمية للعملية برمتها. عندما ننقل البيانات من التيار الرئيسي إلى العامل ، يحصل العامل على نسخة فقط.
يمكننا وصف الدفق الرئيسي ودفق العامل في ملف واحد. توفر الوحدة worker_threads واجهة برمجة تطبيقات يمكننا من خلالها تحديد مؤشر الترابط الذي يتم تنفيذه حاليًا.

أشارك روابط لمصادر مفيدة ورابط لعرض رايان دال عندما قدم حلقة الأحداث (من المثير للاهتمام أن نرى).
حلقة الحدث
- ترجمة مقال من وثائق Node.js
- https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
- https://habr.com/ru/post/336498/
Worker_threads
- https://nodejs.org/api/worker_threads.html#worker_threads_worker_workerdata - API
- https://habr.com/ru/company/ruvds/blog/415659/
- https://nodesource.com/blog/worker-threads-nodejs/
- شرائح أصلية من عرض Ryan Dahl (عبر VPN)