في الآونة الأخيرة ، تم إصدار الإصدار 10.5.0 من منصة Node.js. كانت إحدى ميزاته الرئيسية هي دعم العمل مع التدفقات التي تمت إضافتها لأول مرة إلى Node.js ، بينما لا تزال تجريبية. هذه الحقيقة مثيرة للاهتمام بشكل خاص في ضوء حقيقة أن النظام الأساسي لديه الآن هذه الفرصة ، التي كان أتباعها فخورون دائمًا بحقيقة أنها لا تحتاج إلى تدفقات بسبب النظام الفرعي I / O غير المتزامن الرائع. ومع ذلك ، ظهر دعم مؤشر الترابط في Node.js. لماذا يكون ذلك؟ لمن ولماذا يمكن أن يكونوا في متناول اليد؟

باختصار ، هذا ضروري حتى يمكن لمنصة Node.js أن تصل إلى آفاق جديدة في تلك المناطق التي لم تظهر فيها في السابق النتائج الأكثر بروزًا. نحن نتحدث عن إجراء العمليات الحسابية التي تستخدم موارد المعالج بشكل مكثف. هذا هو السبب الرئيسي في أن Node.js ليست قوية جدًا في مجالات مثل الذكاء الاصطناعي والتعلم الآلي ومعالجة كميات كبيرة من البيانات. تم توجيه الكثير من الجهد نحو السماح لـ Node.js بإظهار نفسها بشكل جيد في حل مثل هذه المشاكل ، ولكن هنا لا تزال هذه المنصة تبدو أكثر تواضعا من ، على سبيل المثال ، في تطوير الخدمات المصغرة.
يقول مؤلف المادة ، التي ننشر ترجمتها اليوم ، أنه قرر اختزال الوثائق الفنية ، التي يمكن العثور عليها في
طلب السحب الأصلي وفي
المصادر الرسمية ، إلى مجموعة من الأمثلة العملية البسيطة. ويأمل أن يعرف أي شخص يطلع على هذه الأمثلة ما يكفي للبدء في سلاسل الرسائل في Node.js.
حول الوحدة النمطية worker_threads وعلامة العامل التجريبي
يتم تنفيذ دعم تعدد مؤشرات الترابط في Node.js كعنصر
worker_threads
. لذلك ، من أجل الاستفادة من الميزة الجديدة ، يجب توصيل هذه الوحدة باستخدام الأمر
require
.
لاحظ أنه لا يمكنك العمل إلا مع
worker_threads
باستخدام علامة -
experimental-worker
عند تشغيل البرنامج النصي ، وإلا فلن يعثر النظام على هذه الوحدة.
لاحظ أن العلم يتضمن كلمة "عامل" وليس "خيط". بالضبط ما نتحدث عنه مذكور في الوثائق ، التي تستخدم المصطلحين "خيط عامل" (خيط عامل) أو مجرد "عامل" (عامل). في المستقبل ، سوف نتبع نفس النهج.
إذا كنت قد كتبت بالفعل رمزًا متعدد سلاسل المحادثات ، فاستكشاف الميزات الجديدة لـ Node.js ، سترى الكثير من الأشياء التي تعرفها بالفعل. إذا لم تكن قد عملت مع أي شيء مثل هذا من قبل ، فما عليك سوى مواصلة القراءة ، حيث سيتم تقديم التفسيرات المناسبة للقادمين الجدد هنا.
حول المهام التي يمكن حلها بمساعدة العاملين في Node.js
الغرض من تدفقات العمال ، كما سبق ذكره ، هو حل المهام التي تستخدم قدرات المعالج بشكل مكثف. وتجدر الإشارة إلى أن استخدامها لحل مشكلات الإدخال / الإخراج هو إهدار للموارد ، حيث أنه ، وفقًا للوثائق الرسمية ، فإن آليات Node.js الداخلية التي تهدف إلى تنظيم الإدخال / الإخراج غير المتزامن تكون أكثر فاعلية في حد ذاتها من استخدام حل نفس مشكلة تدفق العمال. لذلك ، قررنا على الفور أننا لن نتعامل مع إدخال وإخراج البيانات باستخدام العمال.
لنبدأ بمثال بسيط يوضح كيفية إنشاء العمال واستخدامهم.
المثال رقم 1
const { Worker, isMainThread, workerData } = require('worker_threads'); let currentVal = 0; let intervals = [100,1000, 500] function counter(id, i){ console.log("[", id, "]", i) return i; } if(isMainThread) { console.log("this is the main thread") for(let i = 0; i < 2; i++) { let w = new Worker(__filename, {workerData: i}); } setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[2], "MainThread"); } else { console.log("this isn't") setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[workerData], workerData); }
سيبدو إخراج هذا الرمز مثل مجموعة من الخطوط التي تعرض العدادات التي تزيد قيمها بسرعات مختلفة.
نتائج المثال الأولسنتعامل مع ما يحدث هنا:
- تنشئ التعليمات الموجودة في التعبير
if
2 سلسلة __filename
، يتم أخذ الرمز الخاص بها ، بفضل معلمة __filename
، من نفس البرنامج النصي الذي مرره Node.js عند تشغيل المثال. يحتاج العمال الآن إلى المسار الكامل للملف الذي يحتوي على الكود ، فهم لا يدعمون المسارات النسبية ، ولهذا السبب يتم استخدام هذه القيمة هنا.
- يتم إرسال البيانات إلى هذين العاملين كمعلمة عالمية ، في شكل السمة
workerData
، والتي يتم استخدامها في الوسيطة الثانية. بعد ذلك ، يمكن الوصول إلى هذه القيمة من خلال ثابت يحمل نفس الاسم (انتبه إلى كيفية إنشاء الثابت المقابل في السطر الأول من الملف وكيف يتم استخدامه في السطر الأخير).
في ما يلي مثال بسيط جدًا على استخدام وحدة
worker_threads
، ولا يحدث شيء مثير للاهتمام هنا حتى الآن. لذلك ، فكر في مثال آخر.
المثال رقم 2
ضع في اعتبارك مثالاً ، أولاً ، سنقوم ببعض الحسابات "الثقيلة" ، وثانيًا ، نقوم بشيء غير متزامن في الخيط الرئيسي.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); if(isMainThread) { console.log("This is the main thread") let w = new Worker(__filename, {workerData: null}); w.on('message', (msg) => { // ! console.log("First value is: ", msg.val); console.log("Took: ", (msg.timeDiff / 1000), " seconds"); }) w.on('error', console.error); w.on('exit', (code) => { if(code != 0) console.error(new Error(`Worker stopped with exit code ${code}`)) }); request.get('http://www.google.com', (err, resp) => { if(err) { return console.error(err); } console.log("Total bytes received: ", resp.body.length); }) } else { // function random(min, max) { return Math.random() * (max - min) + min } const sorter = require("./list-sorter"); const start = Date.now() let bigList = Array(1000000).fill().map( (_) => random(1,10000)) sorter.sort(bigList); parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start}); }
لتشغيل هذا المثال ، انتبه إلى حقيقة أن هذا الرمز يحتاج إلى وحدة
request
(يمكن تثبيته باستخدام npm ، على سبيل المثال ، باستخدام
npm init --yes
npm install request --save
وأوامر
npm install request --save
في دليل فارغ مع الملف الذي يحتوي على الرمز أعلاه
npm install request --save
) ، وحقيقة أنه يستخدم الوحدة النمطية المساعدة ، التي يتم توصيلها بواسطة الأمر
const sorter = require("./list-sorter");
التابع للأمر
const sorter = require("./list-sorter");
. يجب أن يكون ملف هذه الوحدة النمطية (
list-sorter.js
) في نفس مكان الملف الموصوف أعلاه ، ويبدو
list-sorter.js
كما يلي:
module.exports = { firstValue: null, sort: function(list) { let sorted = list.sort(); this.firstValue = sorted[0] } }
هذه المرة نحن نحل مشكلتين في نفس الوقت. أولاً ، نقوم بتحميل الصفحة الرئيسية لموقع google.com ، وثانيًا ، نقوم بفرز مجموعة عشوائية من مليون رقم. قد يستغرق ذلك بضع ثوانٍ ، مما يمنحنا فرصة رائعة لمشاهدة آليات Node.js الجديدة قيد التنفيذ. بالإضافة إلى ذلك ، نقيس هنا الوقت الذي يستغرقه مؤشر ترابط العامل لفرز الأرقام ، وبعد ذلك نرسل نتيجة القياس (مع العنصر الأول للصفيف الذي تم فرزه) إلى التدفق الرئيسي ، الذي يعرض النتائج في وحدة التحكم.
نتيجة المثال الثانيفي هذا المثال ، أهم شيء هو إظهار آلية تبادل البيانات بين الخيوط.
يمكن للعمال تلقي رسائل من الخيط الرئيسي بفضل طريقة التشغيل. في الكود يمكنك أن تجد الأحداث التي نستمع إليها. يتم تشغيل حدث
message
كل مرة نرسل فيها رسالة من مؤشر ترابط معين باستخدام طريقة
parentPort.postMessage
. بالإضافة إلى ذلك ، يمكن استخدام نفس الطريقة لإرسال رسالة إلى مؤشر ترابط عن طريق الوصول إلى مثيل عامل وتلقيها باستخدام كائن
parentPort
.
الآن دعونا نلقي نظرة على مثال آخر ، يشبه إلى حد كبير ما رأيناه بالفعل ، ولكن هذه المرة سنولي اهتمامًا خاصًا بهيكل المشروع.
المثال رقم 3
كمثال أخير ، نقترح النظر في تنفيذ نفس الوظيفة كما في المثال السابق ، ولكن هذه المرة سنقوم بتحسين هيكل الشفرة ، وجعلها أكثر نظافة ، وإحضارها إلى نموذج يعزز راحة دعم مشروع برنامج.
هنا هو رمز البرنامج الرئيسي.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); function startWorker(path, cb) { let w = new Worker(path, {workerData: null}); w.on('message', (msg) => { cb(null, msg) }) w.on('error', cb); w.on('exit', (code) => { if(code != 0) console.error(new Error(`Worker stopped with exit code ${code}`)) }); return w; } console.log("this is the main thread") let myWorker = startWorker(__dirname + '/workerCode.js', (err, result) => { if(err) return console.error(err); console.log("[[Heavy computation function finished]]") console.log("First value is: ", result.val); console.log("Took: ", (result.timeDiff / 1000), " seconds"); }) const start = Date.now(); request.get('http://www.google.com', (err, resp) => { if(err) { return console.error(err); } console.log("Total bytes received: ", resp.body.length); //myWorker.postMessage({finished: true, timeDiff: Date.now() - start}) // })
وهنا الكود الذي يصف سلوك خيط العمل (في البرنامج أعلاه ، يتم تشكيل المسار إلى الملف الذي يحتوي على هذا الكود باستخدام بناء
__dirname + '/workerCode.js'
):
const { parentPort } = require('worker_threads'); function random(min, max) { return Math.random() * (max - min) + min } const sorter = require("./list-sorter"); const start = Date.now() let bigList = Array(1000000).fill().map( (_) => random(1,10000)) sorter.sort(bigList); parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start});
فيما يلي ميزات هذا المثال:
- الآن يوجد رمز لمؤشر الترابط الرئيسي ولمؤشر الترابط العامل في ملفات مختلفة. هذا يسهل دعم وتوسيع المشروع.
- تُرجع الدالة
startWorker
جديدًا للعامل ، والذي يسمح ، إذا لزم الأمر ، بإرسال رسائل إلى هذا العامل من التدفق الرئيسي. - ليست هناك حاجة للتحقق مما إذا كان الرمز يتم تنفيذه في سلسلة المحادثات الرئيسية (لقد أزلنا
if
مع التحقق المطابق). - يُظهر العامل جزءًا من التعليمات البرمجية التي تم التعليق عليها يوضح آلية تلقي الرسائل من الدفق الرئيسي ، والتي ، بالنظر إلى آلية إرسال الرسائل التي تمت مناقشتها بالفعل ، تسمح بتبادل البيانات غير المتزامن ثنائي الاتجاه بين الدفق الرئيسي ودفق العامل.
الملخص
في هذه المقالة ، قمنا ، باستخدام أمثلة عملية ، بفحص ميزات استخدام القدرات الجديدة للعمل مع التدفقات في Node.js. إذا كنت قد أتقنت ما تمت مناقشته هنا ، فهذا يعني أنك مستعد لرؤية وثائقك وبدء
worker_threads
الخاصة مع وحدة
worker_threads
. ربما تجدر الإشارة إلى أن هذه الميزة ظهرت فقط في Node.js ، في حين أنها تجريبية ، لذلك بمرور الوقت ، قد يتغير شيء ما في تنفيذها. بالإضافة إلى ذلك ، إذا واجهت أخطاء أثناء
worker_threads
الخاصة مع
worker_threads
أو وجدت أن هذه الوحدة لا تتداخل مع بعض الميزات المفقودة منها ، فأخبر المطورين بمعلومات عنها وساعد في تحسين النظام الأساسي Node.js.
أعزائي القراء! ما رأيك في دعم multithreading في Node.js؟ هل تخطط لاستخدام هذه الميزة في مشاريعك؟