في 18 يناير ،
تم الإعلان عن إصدار منصة Node.js
11.7.0 . من بين التغييرات الملحوظة في هذا الإصدار ، يمكن للمرء أن يلاحظ الاستنتاج من فئة work_threads الوحدة النمطية التجريبية ، والتي ظهرت في Node.js
10.5.0 . الآن العلم - العامل التجريبي ليست هناك حاجة لاستخدامها. ظلت هذه الوحدة ، منذ نشأتها ، مستقرة إلى حد ما ، وبالتالي تم
اتخاذ القرار ، كما هو موضح في Node.js 11.7.0.

يعرض مؤلف المادة ، التي ننشر ترجمتها ، مناقشة إمكانات الوحدة النمطية للعاملين ، ولا سيما ، فهو يريد أن يتحدث عن سبب الحاجة إلى هذه الوحدة ، وكيف يتم تطبيق تعدد العمليات في JavaScript و Node.js لأسباب تاريخية. سوف نتحدث هنا عن المشكلات المرتبطة بكتابة تطبيقات JS متعددة الخيوط ، والطرق الحالية لحلها ، وحول مستقبل معالجة البيانات الموازية باستخدام ما يسمى بـ "مؤشرات ترابط العامل" ، والتي تسمى أحيانًا "مؤشرات ترابط العامل" أو مجرد "العمال".
الحياة في عالم واحد مترابطة
تم تصميم JavaScript كلغة برمجة ذات ترابط واحد يتم تشغيلها في المستعرض. تعني كلمة "الخيوط المفردة" أنه في نفس العملية (في المتصفحات الحديثة التي نتحدث عن علامات تبويب منفصلة للمتصفح) ، يمكن تنفيذ مجموعة واحدة فقط من التعليمات في وقت واحد.
هذا يبسط تطوير التطبيق ، ويسهل عمل المبرمجين. في البداية ، كانت JavaScript لغة مناسبة فقط لإضافة بعض الميزات التفاعلية إلى صفحات الويب ، على سبيل المثال ، شيء مثل التحقق من صحة النموذج. من بين المهام التي تم تصميم JS لها ، لم يكن هناك أي شيء معقد بشكل خاص يتطلب تعدد العمليات.
رأى
Ryan Dahl ، منشئ Node.js ، فرصة مثيرة للاهتمام في هذا التقييد اللغوي. لقد أراد تطبيق نظام خادم يستند إلى نظام فرعي للإدخال / الإخراج غير متزامن. هذا يعني أن المبرمج لم يكن بحاجة إلى التعامل مع الخيوط ، مما يبسط عملية التطوير لمنصة مماثلة إلى حد كبير. عند تطوير برامج مصممة لتنفيذ التعليمات البرمجية المتوازية ، قد تنشأ مشاكل يصعب حلها. لنقل أنه إذا حاولت عدة مؤشرات ترابط الوصول إلى منطقة الذاكرة نفسها ، فقد يؤدي ذلك إلى ما يسمى "حالة سباق العملية" التي تعطل البرنامج. من الصعب إعادة إنتاج هذه الأخطاء وتصحيحها.
هل النظام الأساسي Node.js مترابط واحد؟
هل تطبيقات Node.js ترابط واحد؟ نعم بطريقة ما. في الواقع ، يسمح لك Node.js بتنفيذ إجراءات معينة بشكل متوازٍ ، ولكن لهذا لا يحتاج المبرمج إلى إنشاء سلاسل رسائل أو مزامنتها. يقوم النظام الأساسي Node.js ونظام التشغيل بإجراء عمليات إدخال / إخراج متوازية باستخدام الوسائل الخاصة بهما ، وعندما يحين الوقت لمعالجة البيانات باستخدام شفرة JavaScript الخاصة بنا ، فإنه يعمل في وضع مترابط واحد.
بمعنى آخر ، كل شيء ما عدا كود JS الخاص بنا يعمل بشكل متوازٍ. في الكتل المتزامنة لرمز JavaScript ، يتم تنفيذ الأوامر دائمًا واحدة تلو الأخرى ، بالترتيب الذي يتم به تقديمها في التعليمات البرمجية المصدر:
let flag = false function doSomething() { flag = true
كل هذا رائع - إذا كانت جميع التعليمات البرمجية الخاصة بنا مشغولة باستخدام I / O غير المتزامن. يتكون البرنامج من كتل صغيرة من الشفرة المتزامنة التي تعمل بسرعة على البيانات ، على سبيل المثال ، يتم إرسالها إلى الملفات والتدفقات. رمز شظايا البرنامج سريع لدرجة أنه لا يمنع تنفيذ التعليمات البرمجية لشظاياها الأخرى. وقت أكثر بكثير مما يستغرقه تنفيذ التعليمات البرمجية لانتظار نتائج الإدخال / الإخراج غير المتزامن. النظر في مثال صغير:
db.findOne('SELECT ... LIMIT 1', function(err, result) { if (err) return console.error(err) console.log(result) }) console.log('Running query') setTimeout(function() { console.log('Hey there') }, 1000)
من المحتمل أن يستغرق الاستعلام عن قاعدة البيانات الموضحة هنا حوالي دقيقة واحدة ، ولكن سيتم إرسال رسالة
Running query
إلى وحدة التحكم فور بدء هذا الاستعلام. في هذه الحالة ، سيتم عرض رسالة
Hey there
ثانية واحدة بعد تنفيذ الطلب ، بغض النظر عما إذا كان تنفيذه قد اكتمل أم لا. يستدعي تطبيق Node.js الخاص بنا ببساطة الوظيفة التي تبدأ الطلب ، بينما لا يتم حظر تنفيذ الكود الآخر. بعد اكتمال الطلب ، سيتم إبلاغ التطبيق بهذا باستخدام وظيفة رد الاتصال ، وبعد ذلك سيتلقى ردًا على هذا الطلب.
المهام المكثفة وحدة المعالجة المركزية
ماذا يحدث إذا كنا بحاجة ، من خلال جافا سكريبت ، إلى القيام بحوسبة ثقيلة؟ على سبيل المثال - لمعالجة مجموعة كبيرة من البيانات المخزنة في الذاكرة؟ قد يؤدي ذلك إلى حقيقة أن البرنامج سيحتوي على جزء من الشفرة المتزامنة ، والتي يستغرق تنفيذها الكثير من الوقت ويمنع تنفيذ التعليمات البرمجية الأخرى. تخيل أن هذه الحسابات تستغرق 10 ثوانٍ. إذا كنا نتحدث عن خادم ويب يعالج طلبًا معينًا ، فهذا يعني أنه لن يكون قادرًا على معالجة الطلبات الأخرى لمدة 10 ثوانٍ على الأقل. هذه مشكلة كبيرة في الواقع ، يمكن أن تؤدي العمليات الحسابية التي تزيد عن 100 مللي ثانية إلى حدوث هذه المشكلة بالفعل.
لم يتم تصميم JavaScript و Node.js في الأصل لحل المهام التي تستخدم موارد المعالج بشكل مكثف. في حالة تشغيل JS في المتصفح ، يعني تنفيذ هذه المهام "الفرامل" على واجهة المستخدم. في Node.js ، يمكن أن يحد ذلك من القدرة على طلب النظام الأساسي لأداء مهام I / O غير متزامنة جديدة والقدرة على الاستجابة للأحداث المرتبطة بإتمامها.
دعنا نعود إلى مثالنا السابق. تخيل أنه استجابةً لاستعلام قاعدة البيانات ، ظهرت عدة آلاف من السجلات المشفرة ، والتي يجب فك تشفيرها في كود JS المتزامن:
db.findAll('SELECT ...', function(err, results) { if (err) return console.error(err)
النتائج ، بعد استلامها ، هي في وظيفة رد الاتصال. بعد ذلك ، وحتى نهاية معالجتها ، لا يمكن تنفيذ أي كود JS آخر. عادةً ، كما ذكرنا سابقًا ، يكون الحمل على النظام الذي تم إنشاؤه بواسطة هذا الرمز ضئيلًا ، وسرعان ما يؤدي المهام الموكلة إليه. ولكن في هذه الحالة ، تلقى البرنامج نتائج الاستعلام التي تحتوي على كمية كبيرة ، وما زلنا بحاجة إلى معالجتها. شيء من هذا القبيل يمكن أن يستغرق بضع ثوان. إذا كنا نتحدث عن خادم يعمل معه العديد من المستخدمين ، فإن هذا يعني أنه يمكنهم الاستمرار في العمل فقط بعد الانتهاء من عملية كثيفة الاستخدام للموارد.
لماذا جافا سكريبت أبدا المواضيع؟
بالنظر إلى ما سبق ، قد يبدو أنه لحل مشاكل الحوسبة الثقيلة في Node.js ، يلزمك إضافة وحدة نمطية جديدة تتيح لك إنشاء سلاسل رسائل وإدارتها. كيف يمكنك أن تفعل دون شيء من هذا القبيل؟ من المحزن جدًا أن أولئك الذين يستخدمون نظامًا أساسيًا للخوادم الناضجة ، مثل Node.js ، لا يملكون الوسائل اللازمة لحل المشكلات المرتبطة بمعالجة كميات كبيرة من البيانات بشكل جميل.
كل هذا صحيح ، لكن إذا أضفت القدرة على العمل باستخدام التدفقات في JavaScript ، فسيؤدي ذلك إلى تغيير في طبيعة هذه اللغة. في JS ، لا يمكنك فقط إضافة القدرة على التعامل مع سلاسل الرسائل ، على سبيل المثال ، في شكل مجموعة جديدة من الفئات أو الوظائف. للقيام بذلك ، تحتاج إلى تغيير اللغة نفسها. في اللغات التي تدعم تعدد العمليات ، يتم استخدام مفهوم التزامن على نطاق واسع. على سبيل المثال ، في Java ، حتى
بعض الأنواع الرقمية ليست ذرية. هذا يعني أنه في حالة عدم استخدام آليات التزامن للعمل معهم من مؤشرات ترابط مختلفة ، فقد ينتج عن ذلك ، على سبيل المثال ، بعد محاولة عدة مؤشرات ترابط في وقت واحد لتغيير قيمة المتغير نفسه ، سيتم تعيين عدة بايت من هذا المتغير على واحد تدفق ، وعدد قليل من الآخرين. نتيجة لذلك ، سيحتوي هذا المتغير على شيء غير متوافق مع التشغيل العادي للبرنامج.
الحل البدائي للمشكلة: تكرار حلقة الحدث
لن تقوم Node.js بتنفيذ مقطع التعليمات البرمجية التالي في قائمة انتظار الأحداث حتى اكتمال الكتلة السابقة. هذا يعني أنه لحل مشكلتنا ، يمكننا
setImmediate(callback)
إلى أجزاء ممثلة بشظايا الكود المتزامن ، ثم استخدام إنشاء النموذج
setImmediate(callback)
أجل التخطيط لتنفيذ هذه الأجزاء. سيتم تنفيذ الرمز المحدد بواسطة وظيفة
callback
في هذه البنية بعد الانتهاء من مهام التكرار الحالي (علامة) من حلقة الحدث. بعد ذلك ، يتم استخدام نفس التصميم في قائمة انتظار الدفعة التالية من العمليات الحسابية. هذا يسمح بعدم عرقلة دورة الأحداث ، وفي الوقت نفسه ، حل المشاكل الحجمي.
تخيل أن لدينا مجموعة كبيرة تحتاج إلى معالجة ، بينما تتطلب معالجة كل عنصر من هذه الصفيف حسابات معقدة:
const arr = [] for (const item of arr) {
كما ذكرنا سابقًا ، إذا قررنا معالجة المصفوفة بأكملها في مكالمة واحدة ، فسوف يستغرق الأمر وقتًا طويلاً ومنع تنفيذ رمز تطبيق آخر. لذلك ، سنقوم بتقسيم هذه المهمة الكبيرة إلى أجزاء واستخدام
setImmediate(callback)
:
const crypto = require('crypto') const arr = new Array(200).fill('something') function processChunk() { if (arr.length === 0) {
الآن ،
setImmediate()
واحدة ، نقوم بمعالجة عشرة عناصر من الصفيف ، وبعد ذلك ، باستخدام
setImmediate()
، نخطط الدفعة التالية من العمليات الحسابية. وهذا يعني أنه إذا كنت بحاجة إلى تنفيذ المزيد من التعليمات البرمجية في البرنامج ، فيمكن تنفيذه بين العمليات على معالجة أجزاء الصفيف. لهذا ، هنا ، في نهاية المثال ، هناك كود يستخدم
setInterval()
.
كما ترون ، فإن هذا الرمز يبدو أكثر تعقيدًا من الإصدار الأصلي. غالبًا ما يمكن أن تكون الخوارزمية أكثر تعقيدًا من خوارزمياتنا ، مما يعني أنه عند تنفيذها ، لن يكون من السهل تقسيم الحسابات إلى أجزاء وفهم أين ، من أجل تحقيق التوازن الصحيح ، تحتاج إلى ضبط
setImmediate()
للتخطيط
setImmediate()
التالي من الحسابات. بالإضافة إلى ذلك ، تبين الآن أن الشفرة غير متزامنة ، وإذا كان مشروعنا يعتمد على مكتبات الطرف الثالث ، فقد لا نتمكن من تقسيم عملية حل مهمة صعبة إلى أجزاء.
عمليات الخلفية
ربما
setImmediate()
أعلاه مع
setImmediate()
على العمل بشكل جيد في الحالات البسيطة ، ولكنها بعيدة عن المثالية. بالإضافة إلى ذلك ، لا يتم استخدام سلاسل الرسائل هنا (لأسباب واضحة) ، كما أننا لا ننوي تغيير اللغة لهذا الغرض. هل من الممكن القيام بمعالجة البيانات الموازية دون استخدام مؤشرات الترابط؟ نعم ، هذا ممكن ، ولهذا نحتاج إلى نوع من الآلية لمعالجة بيانات الخلفية. يتعلق الأمر ببدء مهمة معينة ، ونقل البيانات إليها ، وبالتالي فإن هذه المهمة ، دون التدخل في الكود الرئيسي ، تستخدم كل ما تحتاجه ، وتنفق الكثير من الوقت في العمل كما تحتاج ، ثم تُرجع النتائج إلى الرمز الرئيسي. نحتاج إلى شيء مشابه لشفرة التعليمات البرمجية التالية:
الحقيقة هي أنه في Node.js يمكنك استخدام عمليات الخلفية. نحن نتحدث عن حقيقة أنه يمكنك إنشاء مفترق للعملية وتنفيذ خطة العمل الموصوفة أعلاه باستخدام آلية تبادل الرسائل بين العمليات التابعة والوالدين. يمكن أن تتفاعل العملية الرئيسية مع العملية التنازلية ، وإرسال الأحداث إليها واستلامها منها. لا يتم استخدام الذاكرة المشتركة مع هذا النهج. يتم "استنساخ" جميع البيانات التي تتبادلها العمليات ، أي عندما يتم إجراء تغييرات على مثيل لهذه البيانات من خلال عملية واحدة ، فإن هذه التغييرات تكون غير مرئية لعملية أخرى. هذا مشابه لطلب HTTP - عندما يرسله العميل إلى الخادم ، لا يتلقى الخادم سوى نسخة منه. إذا كانت العمليات لا تستخدم الذاكرة المشتركة ، فإن هذا يعني أنه من خلال التشغيل المتزامن ، يكون من المستحيل إنشاء "حالة سباق" ، وأننا لسنا بحاجة إلى إثقال أنفسنا بالعمل مع الخيوط. يبدو تم حل مشكلتنا.
صحيح ، في الواقع هذا ليس كذلك. نعم - أمامنا أحد الحلول لمهمة إجراء عمليات حسابية مكثفة ، لكنها ، مرة أخرى ، ليست مثالية. إنشاء شوكة العملية عملية كثيفة الاستخدام للموارد. يستغرق بعض الوقت لاستكماله. في الواقع ، نحن نتحدث عن إنشاء جهاز افتراضي جديد من نقطة الصفر وعن زيادة مقدار الذاكرة التي يستهلكها البرنامج ، ويرجع ذلك إلى حقيقة أن العمليات لا تستخدم الذاكرة المشتركة. بالنظر إلى ما سبق ، من المناسب أن نسأل ما إذا كان من الممكن ، بعد الانتهاء من مهمة ما ، إعادة استخدام شوكة العملية. يمكنك إعطاء إجابة إيجابية على هذا السؤال ، ولكن عليك هنا أن تتذكر أنه من المخطط نقل شوكة العملية إلى العديد من المهام كثيفة الاستخدام للموارد والتي سيتم تنفيذها بشكل متزامن. يمكن رؤية مشكلتين هنا:
- على الرغم من أن هذا النهج لا يتم حظر العملية الرئيسية ، إلا أن العملية التنازلية قادرة على أداء المهام المنقولة إليها بالتتابع فقط. إذا كانت لدينا مهمتان ، إحداهما تستغرق 10 ثوانٍ ، والثانية تستغرق ثانية واحدة ، وسنقوم بإكمالها بهذا الترتيب ، فمن غير المرجح أن نحب الحاجة إلى الانتظار حتى يكتمل الأول قبل الثاني. نظرًا لأننا نقوم بإنشاء شوكات عملية ، فإننا نرغب في استخدام قدرات نظام التشغيل لتخطيط المهام واستخدام موارد الحوسبة لجميع مراكز المعالج. نحتاج إلى شيء يشبه العمل على جهاز كمبيوتر لشخص يستمع إلى الموسيقى ويسافر عبر صفحات الويب. للقيام بذلك ، يمكنك إنشاء عمليتين شوكة وتنظيم التنفيذ المتوازي للمهام مع مساعدتهم.
- بالإضافة إلى ذلك ، إذا أدت إحدى المهام إلى نهاية العملية مع وجود خطأ ، فلن تتم معالجة جميع المهام المرسلة إلى هذه العملية.
من أجل حل هذه المشكلات ، نحتاج إلى العديد من عمليات الشوكة ، وليس واحدة منها ، ولكن سيتعين علينا الحد من عددها ، حيث أن كل واحدة منها تستهلك موارد النظام وتستغرق إنشاء كل منها وقتًا. نتيجة لذلك ، وبعد نمط الأنظمة التي تدعم اتصالات قاعدة البيانات ، نحتاج إلى شيء مثل مجموعة من العمليات الجاهزة للاستخدام. سيستخدم نظام إدارة تجمع العمليات ، عند استلام المهام الجديدة ، العمليات المجانية لإكمالها ، وعندما تتعامل العملية مع المهمة ، ستتمكن من تعيين عملية جديدة لها. هناك شعور بأن خطة العمل هذه ليست سهلة التنفيذ ، بل إنها في الواقع كذلك. سوف نستخدم حزمة
مزرعة العمال لتنفيذ هذا المخطط:
وحدة Worker_threads
لذلك ، هل تم حل مشكلتنا؟ نعم ، يمكننا القول أنه تم حلها ، ولكن مع هذا النهج ، هناك حاجة إلى ذاكرة أكبر بكثير مما سيكون ضروريًا إذا كان لدينا حل متعدد مؤشرات الترابط تحت تصرفنا. تستهلك مؤشرات الترابط موارد أقل بكثير مقارنة بالشوكة العملية. هذا هو السبب في أن الوحدة النمطية
worker_threads
ظهرت في
worker_threads
تعمل مؤشرات ترابط العامل في سياق معزول. انهم تبادل المعلومات مع العملية الرئيسية باستخدام الرسائل. هذا ينقذنا من مشكلة "حالة السباق" التي تخضع لها البيئات المتعددة الخيوط. في الوقت نفسه ، توجد تدفقات العمال في نفس العملية مثل البرنامج الرئيسي ، أي باستخدام هذا النهج ، مقارنةً باستخدام شوكات العمليات ، يتم استخدام ذاكرة أقل بكثير.
بالإضافة إلى ذلك ، من خلال العمل مع العمال ، يمكنك استخدام الذاكرة المشتركة. لذلك ، خصيصًا لهذا الغرض ، يتم
SharedArrayBuffer
كائنات من نوع
SharedArrayBuffer
. يجب استخدامها فقط في تلك الحالات التي يحتاج فيها البرنامج إلى إجراء معالجة معقدة لكميات كبيرة من البيانات. إنها تتيح لك حفظ الموارد اللازمة لتسلسل البيانات وإلغاء تسلسلها عند تنظيم تبادل البيانات بين العمال والبرنامج الرئيسي من خلال الرسائل.
تدفقات عامل العمال
إذا كنت تستخدم النظام الأساسي Node.js قبل الإصدار 11.7.0 ، ثم لتمكين العمل مع الوحدة النمطية
worker_threads
، فإنك تحتاج إلى استخدام علامة
--experimental-worker
عند بدء
--experimental-worker
بالإضافة إلى ذلك ، يجدر بنا أن نتذكر أن إنشاء عامل (مثل إنشاء سلسلة رسائل بأي لغة) ، على الرغم من أنه يتطلب موارد أقل بكثير من إنشاء مفترق للعملية ، إلا أنه ينشئ أيضًا عبءًا معينًا على النظام. ربما في حالتك حتى هذا الحمل قد يكون أكثر من اللازم. في مثل هذه الحالات ، توصي الوثائق بإنشاء مجموعة من العمال. إذا كنت بحاجة إلى ذلك ، فبالتأكيد يمكنك إنشاء تطبيقك الخاص لهذه الآلية ، ولكن ربما يجب عليك البحث عن شيء مناسب في سجل NPM.
النظر في مثال للعمل مع المواضيع عامل. سيكون لدينا ملف رئيسي ،
index.js
، سنقوم بإنشاء مؤشر ترابط عامل به وتمريره بعض البيانات للمعالجة. تعتمد واجهة برمجة التطبيقات المناظرة على الحدث ، لكنني سأستخدم وعدًا هنا يحل عند وصول الرسالة الأولى من العامل:
كما ترون ، فإن استخدام آلية تدفق سير العمل أمر بسيط للغاية. وهي ، عند إنشاء عامل ، تحتاج إلى تمرير المسار إلى الملف مع رمز العامل والبيانات إلى مصمم
Worker
. تذكر أن هذه البيانات مستنسخة ولا يتم تخزينها في الذاكرة المشتركة. بعد بدء العامل ، نتوقع رسالة منه ، نستمع إلى حدث
message
.
أعلاه ، عند إنشاء كائن من النوع
Worker
، مررنا المُنشئ اسم الملف برمز العامل -
service.js
. هنا هو رمز لهذا الملف:
const { workerData, parentPort } = require('worker_threads')
هناك شيئان يثيران اهتمامنا بقانون العمال. أولاً ، نحن بحاجة إلى البيانات المرسلة بواسطة التطبيق الرئيسي. في حالتنا ، يتم تمثيلهم بواسطة متغير
workerData
. ثانياً ، نحتاج إلى آلية لنقل المعلومات إلى التطبيق الرئيسي. يتم تمثيل هذه الآلية بواسطة كائن
parentPort
، الذي يحتوي على طريقة
postMessage()
، باستخدام التي
postMessage()
بها نتائج معالجة البيانات إلى التطبيق الرئيسي. هذا كيف يعمل كل شيء.
هنا مثال بسيط للغاية ، ولكن باستخدام نفس الآليات ، يمكنك بناء هياكل أكثر تعقيدًا. على سبيل المثال ، من تدفق عامل ، يمكنك إرسال الكثير من الرسائل إلى ساحة المشاركات الرئيسية التي تحمل معلومات حول حالة معالجة البيانات في حالة احتياج تطبيقنا إلى آلية مماثلة. حتى من العامل ، يمكن إرجاع نتائج معالجة البيانات في أجزاء. على سبيل المثال ، قد يكون شيء من هذا القبيل مفيدًا في الموقف عندما يكون العامل مشغولا ، على سبيل المثال ، معالجة الآلاف من الصور ، وأنت ، دون انتظار معالجتها كلها ، ترغب في إخطار التطبيق الرئيسي باستكمال معالجة كل منها.
يمكن الاطلاع
هنا على تفاصيل وحدة
worker_threads
.
عمال الويب
ربما تكون قد سمعت عن عمال الويب. تم تصميمها للاستخدام في بيئة العميل ، وقد وجدت هذه التكنولوجيا لفترة طويلة وتتمتع
بدعم جيد للمتصفحات الحديثة. تختلف واجهة برمجة التطبيقات (API) للعمل مع العاملين على الويب عن ما
worker_threads
الوحدة النمطية Node.js
worker_threads
، كل ما يتعلق بالفروق في البيئات التي يعملون فيها. ومع ذلك ، يمكن لهذه التقنيات حل مشاكل مماثلة. على سبيل المثال ، يمكن استخدام عمال الويب في تطبيقات العميل لأداء تشفير وفك تشفير البيانات وضغطها وإلغاء ضغطها. من خلال مساعدتهم ، يمكنك معالجة الصور وتنفيذ أنظمة رؤية الكمبيوتر (على سبيل المثال ، نحن نتحدث عن التعرف على الوجوه) وحل المشكلات الأخرى المشابهة في المتصفح.
ملخص
worker_threads
— Node.js. , , . , , , « ». , ? ,
worker_threads
, Node.js
worker-farm ,
worker_threads
, Node.js .
أعزائي القراء! Node.js-?
