
نعلم جميعًا أن Node.js يستخدم نموذج إدخال/إخراج غير متزامن يعتمد على الأحداث ويحدد أنه لا يمكنه الاستفادة من وحدة المعالجة المركزية متعددة النواة وأنه ليس جيدًا في إكمال بعض العمليات غير المتعلقة بالإدخال/الإخراج ( مثل تنفيذ البرامج النصية)، وحوسبة الذكاء الاصطناعي، ومعالجة الصور، وما إلى ذلك)، من أجل حل مثل هذه المشكلات، يوفر Node.js حلاً تقليديًا متعدد العمليات (السلسلة) (للمناقشات حول العمليات والخيوط، يرجى الرجوع إلى المؤلف مقالة أخرى Node.js وConcurrency Model)، ستعرفك هذه المقالة على آلية الخيوط المتعددة لـ Node.js.
child_process عملية فرعية لـ Node.js لإكمال بعض المهام الخاصة (مثل تنفيذ البرامج النصية). توفر هذه الوحدة بشكل أساسي الأساليب exec و execFile و fork و spwan وغيرها من الأساليب . يستخدم.
const { exec } = require('child_process');
exec('ls -al', (خطأ, stdout, stderr) => {
console.log(stdout);
}); تقوم هذه الطريقة بمعالجة سلسلة الأمر وفقًا للملف القابل للتنفيذ المحدد بواسطة options.shell ، وتخزين مخرجاتها مؤقتًا أثناء تنفيذ الأمر، ثم إرجاع نتيجة التنفيذ في شكل معلمات وظيفة رد الاتصال حتى اكتمال تنفيذ الأمر.
يتم شرح معلمات هذه الطريقة على النحو التالي:
command : الأمر الذي سيتم تنفيذه (مثل ls -al )؛
options المعلمات (اختياري)، الخصائص ذات الصلة هي كما يلي:
cwd : دليل العمل الحالي للعملية الفرعية القيمة الافتراضية هي قيمة process.cwd() ;
shell
env القيمة الافتراضية هي قيمة ترميز process.env
encoding ؛ القيمة الافتراضية هي: utf8 ؛
الملف الذي يعالج سلاسل الأوامر، القيمة الافتراضية على Unix هي /bin/sh ، القيمة الافتراضية على Windows هي قيمة process.env.ComSpec (إذا كانت فارغة، فهي cmd.exe )؛ على سبيل المثال:
const { exec } = يتطلب('child_process');
exec("print('Hello World!')", { shell: 'python' }, (error, stdout, stderr) => {
console.log(stdout);
}); سيؤدي تشغيل المثال أعلاه إلى إخراج Hello World! وهو ما يعادل العملية الفرعية التي تنفذ الأمر python -c "print('Hello World!')" لذلك، عند استخدام هذه السمة، عليك الانتباه إلى ما يلي يجب دعم الملف القابل للتنفيذ المحدد. تنفيذ البيانات ذات الصلة من خلال الخيار -c .
ملاحظة: يحدث أن Node.js يدعم أيضًا الخيار -c ، ولكنه مكافئ لخيار --check ، ويتم استخدامه فقط لاكتشاف ما إذا كانت هناك أخطاء في بناء الجملة في البرنامج النصي المحدد ولن يتم تنفيذ البرنامج النصي ذي الصلة.
signal : استخدم AbortSignal المحدد لإنهاء العملية الفرعية. هذه السمة متاحة فوق الإصدار 14.17.0، على سبيل المثال:
const { exec } = require('child_process');
const ac = new AbortController();
exec('ls -al', { signal: ac.signal }, (error, stdout, stderr) => {}); في المثال أعلاه، يمكننا إنهاء العملية الفرعية مبكرًا عن طريق استدعاء ac.abort() .
timeout : وقت انتهاء العملية الفرعية (إذا كانت قيمة هذه السمة أكبر من 0 ، فعندما يتجاوز وقت تشغيل العملية الفرعية القيمة المحددة، سيتم إرسال إشارة الإنهاء المحددة بواسطة السمة killSignal إلى العملية الفرعية )، بالملليمتر، القيمة الافتراضية هي 0 ؛
killSignal
maxBuffer 1024 * 1024 الأقصى لذاكرة التخزين المؤقت (الثنائية) المسموح بها بواسطة stdout أو stderr، سيتم قتل العملية الفرعية واقتطاع أي مخرجات
: إشارة إنهاء العملية الفرعية، القيمة الافتراضية هي SIGTERM ؛
uid : uid لتنفيذ العملية الفرعية؛
gid gid العملية الفرعية
windowsHide : ما إذا كان سيتم إخفاء نافذة وحدة التحكم للعملية الفرعية، والتي تُستخدم بشكل شائع في أنظمة Windows ، القيمة الافتراضية false ؛
callback : وظيفة رد الاتصال، بما في ذلك المعلمات error و stdout و stderr :
error : إذا تم تنفيذ سطر الأوامر بنجاح، تكون القيمة null ، وإلا فإن القيمة هي مثيل للخطأ، حيث يكون error.code هو المخرج رمز الخطأ للعملية الفرعية error.signalencoding stderr stdout encoding buffer stdout stderr أو إذا كانت قيمة stdout أو stderr عبارة عن سلسلة لا يمكن التعرف عليها، فسيتم تشفيرها وفقًا buffer .const { execFile } = require('child_process');
execFile('ls', ['-al'], (خطأ, stdout, stderr) => {
console.log(stdout);
}); وظيفة هذه الطريقة مشابهة لـ exec والفرق الوحيد هو أن execFile يعالج الأمر مباشرة مع الملف القابل للتنفيذ المحدد (أي قيمة file المعلمة ) بشكل افتراضي، مما يجعل كفاءته أعلى قليلاً من exec (إذا نظرت إلى Shell عندما يتعلق الأمر بمنطق المعالجة، أشعر أن الكفاءة ضئيلة).
يتم شرح معلمات هذه الطريقة على النحو التالي:
file : اسم الملف القابل للتنفيذ أو مساره؛
args : قائمة المعلمات للملف القابل للتنفيذ
: options المعلمة (لا يمكن تحديدها)، والخصائص ذات الصلة هي كما يلي:
shell : عندما تكون القيمة false فهذا يعني استخدام الملف القابل للتنفيذ المحدد مباشرة (أي قيمة file المعلمة) لمعالجة الأمر. عندما تكون القيمة true أو سلاسل أخرى، فإن الوظيفة تعادل shell في exec . القيمة الافتراضية false ؛windowsVerbatimArguments : ما إذا كان سيتم تجاهل المعلمات في Windows أو الهروب منها، وسيتم تجاهل Unix falseuid env cwd killSignal timeout maxBuffer encoding gid تم تقديم كل من windowsHide و signal أعلاه ولن يتم تكرارها هنا.callback : وظيفة رد الاتصال، والتي تعادل callback في exec ولن يتم شرحها هنا.
const { fork } = require('child_process');
صدى const = شوكة('./echo.js', {
الصمت : صحيح
});
echo.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
echo.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
echo.on('إغلاق', (كود) => {
console.log("تم الخروج من العملية الفرعية بالرمز ${code}`);
}); يتم استخدام هذه الطريقة لإنشاء مثيل Node.js جديد لتنفيذ البرنامج النصي Node.js المحدد والتواصل مع العملية الأصلية من خلال IPC.
يتم شرح معلمات هذه الطريقة على النحو التالي:
modulePath : مسار البرنامج النصي Node.js الذي سيتم تشغيله؛
options
args المعلمات التي تم تمريرها إلى البرنامج النصي Node.js؛
مثل:
detached : انظر أدناه للحصول على وصف spwan لـ options.detached ؛
execPath : إنشاء ملف قابل للتنفيذ للعملية الفرعية؛
execArgv : تم تمرير قائمة معلمات السلسلة إلى الملف القابل للتنفيذ، والقيمة الافتراضية هي process.execArgv
serialization : نوع الرقم التسلسلي للرسالة بين العمليات، والقيم المتاحة هي json و advanced ، والقيمة الافتراضية هي json ؛
slient كان true ، فسيتم تمرير stdin و stdout و stderr للعملية الفرعية إلى العملية الأصلية من خلال الأنابيب، وإلا فسيتم توريث القيمة false للعملية الأصلية stdin stdout stderr
options.stdio stdio spwan ما يجب ملاحظته هنا هو أنه
slient ؛ipc (مثل [0, 1, 2, 'ipc'] )، وإلا فسيتم تضمينه. سيتم طرح الاستثناء.الخصائص cwd و env uid و gid و windowsVerbatimArguments و signal و timeout و killSignal تم تقديمها أعلاه ولن يتم تكرارها هنا.
const { spawn } = require('child_process');
const ls = spawn('ls', ['-al']);
ls.stdout.on('بيانات', (بيانات) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('إغلاق', (كود) => {
console.log("تم الخروج من العملية الفرعية بالرمز ${code}`);
}); هذه الطريقة هي الطريقة الأساسية لوحدة child_process exec و execFile و fork التي ستستدعي في النهاية spawn لإنشاء عملية فرعية.
يتم شرح معلمات هذه الطريقة على النحو التالي:
command : اسم الملف القابل للتنفيذ أو مساره؛
args : قائمة المعلمات التي تم تمريرها إلى الملف القابل للتنفيذ
options : إعدادات المعلمة (لا يمكن تحديدها)، والسمات ذات الصلة هي كما يلي:
argv0 : تم إرساله إلى قيمة argv[0] للعملية الفرعية، والقيمة الافتراضية هي قيمة command المعلمة؛
detached : ما إذا كان سيتم السماح للعملية الفرعية بالعمل بشكل مستقل عن العملية الأصلية (أي، بعد خروج العملية الأصلية، سيتم يمكن أن تستمر العملية في التشغيل)، القيمة الافتراضية هي false ، وعندما تكون قيمتها true ، يكون التأثير لكل نظام أساسي كما يلي:
Windows ، بعد خروج العملية الأصلية، يمكن للعملية الفرعية الاستمرار في التشغيل، والعملية الفرعية لديها نافذة وحدة التحكم الخاصة بها (بمجرد بدء تشغيل هذه الميزة، لا يمكن تغييرها أثناء عملية التشغيل)Windows ، ستكون العملية الفرعية بمثابة قائد مجموعة جلسة العملية الجديدة في هذا الوقت، بغض النظر إذا كانت العملية الفرعية منفصلة عن العملية الأصلية، فيمكن أن تستمر العملية الفرعية في العمل بعد خروج العملية الأصلية.تجدر الإشارة إلى أنه إذا كانت العملية الفرعية بحاجة إلى أداء مهمة طويلة الأمد وتريد خروج العملية الأصلية مبكرًا، فيجب استيفاء النقاط التالية في نفس الوقت:
unref الخاص بالعملية الفرعية لإزالة العملية الفرعيةstdio ignoredetached trueعلى سبيل المثال المثال التالي:
// hello.js
const fs = require('fs');
دع الفهرس = 0؛
تشغيل الدالة () {
setTimeout(() => {
fs.writeFileSync('./hello', `index: ${index}`);
إذا (الفهرس <10) {
الفهرس += 1;
يجري()؛
}
}, 1000);
}
يجري()؛
// main.js
const { spawn } = require('child_process');
const Child = spawn('node', ['./hello.js'], {
منفصل: صحيح،
ستديو: "تجاهل"
});
Child.unref();stdio : تكوين الإدخال والإخراج القياسي للعملية الفرعية، القيمة الافتراضية هي pipe ، والقيمة عبارة عن سلسلة أو مصفوفة:
pipe إلى ['pipe', 'pipe', 'pipe'] )، القيم المتاحة هي pipe ، overlapped ، ignore ، inherit ؛stdin و stdout و stderr على التوالي، كل القيم المتاحة للعنصر هي pipe ، overlapped ، ignore ، inherit ، ipc ، كائن دفق، عدد صحيح موجب (واصف الملف مفتوح في العملية الرئيسية)، null (إذا كان الموجود في العناصر الثلاثة الأولى من المصفوفة، فهو يعادل pipe ، وإلا فإنه يعادل ignore ) ، undefined (إذا كان موجودًا في العناصر الثلاثة الأولى من المصفوفة، فهو يعادل pipe ، وإلا فإنه يعادل ignore ).السمات cwd ، env ، uid ، gid ، serialization ، shell (القيمة boolean أو string ) ، windowsVerbatimArguments ، windowsHide ، signal ، timeout ، killSignal تم تقديمها أعلاه ولن يتم تكرارها هنا.
ما ورد أعلاه يقدم مقدمة موجزة عن استخدام الطرق الرئيسية في وحدة child_process ، نظرًا لأن الأساليب execSync و execFileSync و forkSync و spwanSync هي إصدارات متزامنة من exec و execFile و spwan ، فلا يوجد فرق في معلماتها، لذا لن تتكرر.
cluster Node.js من خلال إضافة عملية Node.js إلى المجموعة، يمكننا الاستفادة بشكل كامل من مزايا النواة المتعددة وتوزيع مهام البرنامج على عمليات مختلفة لتحسين التنفيذ. كفاءة البرنامج أدناه، سنستخدم هذا المثال يقدم استخدام وحدة cluster :
const http = require('http');
كتلة ثابتة = تتطلب('الكتلة');
const numCPUs = require('os').cpus().length;
إذا (cluster.isPrimary) {
لـ (دع i = 0؛ i < numCPUs؛ i++) {
block.fork();
}
} آخر {
http.createServer((req, res) => {
res.writeHead(200);
res.end(`${process.pid}n`);
}).listen(8000);
} ينقسم المثال أعلاه إلى جزأين بناءً على حكم سمة cluster.isPrimary (أي الحكم على ما إذا كانت العملية الحالية هي العملية الرئيسية):
cluster.fork ؛8000 ).قم بتشغيل المثال أعلاه وقم بالوصول إلى http://localhost:8000/ في المتصفح، وسنجد أن pid الذي تم إرجاعه يختلف لكل عملية وصول، مما يوضح أن الطلب يتم توزيعه بالفعل على كل عملية فرعية. استراتيجية موازنة التحميل الافتراضية التي تتبناها Node.js هي جدولة دائرية، والتي يمكن تعديلها من خلال متغير البيئة NODE_CLUSTER_SCHED_POLICY أو خاصية cluster.schedulingPolicy :
NODE_CLUSTER_SCHED_POLICY = rr // أو none
هناك شيء آخر يجب ملاحظته وهو أنه على الرغم من أن كل عملية فرعية قد أنشأت خادم HTTP واستمعت إلى نفس المنفذ، إلا أن هذا لا يعني أن هذه العمليات الفرعية حرة في التنافس عليها
.
طلبات المستخدم، لأن هذا لا يضمن موازنة حمل جميع العمليات الفرعية. لذلك، يجب أن تكون العملية الصحيحة هي أن تستمع العملية الرئيسية إلى المنفذ، ثم تعيد توجيه طلب المستخدم إلى عملية فرعية محددة للمعالجة وفقًا لسياسة التوزيع.
وبما أن العمليات معزولة عن بعضها البعض، فإن العمليات تتواصل بشكل عام من خلال آليات مثل الذاكرة المشتركة، وتمرير الرسائل، والأنابيب. تُكمل Node.js الاتصال بين العمليات الرئيسية والتابعة من خلال消息传递، مثل المثال التالي:
const http = require('http');
كتلة ثابتة = تتطلب('الكتلة');
const numCPUs = require('os').cpus().length;
إذا (cluster.isPrimary) {
لـ (دع i = 0؛ i < numCPUs؛ i++) {
عامل ثابت = block.fork();
عامل.ون('رسالة', (رسالة) => {
console.log(`أنا أساسي(${process.pid})، تلقيت رسالة من العامل: "${message}"`);
عامل.إرسال ("أرسل رسالة إلى العامل")
});
}
} آخر {
عملية.on('message', (message) => {
console.log(`أنا عامل(${process.pid})، تلقيت رسالة من الأساسي: "${message}"`)
});
http.createServer((req, res) => {
res.writeHead(200);
res.end(`${process.pid}n`);
عملية.send("إرسال رسالة إلى الأساسي");
}).listen(8000);
} قم بتشغيل المثال أعلاه وقم بزيارة http://localhost:8000/ ، ثم تحقق من الوحدة الطرفية، وسنرى مخرجات مشابهة لما يلي:
أنا أساسي (44460)، تلقيت رسالة من العامل: "أرسل رسالة إلى الأساسي" أنا عامل (44461)، وصلتني رسالة من الأساسي: "أرسل رسالة إلى العامل" أنا أساسي (44460)، وصلتني رسالة من العامل: "أرسل رسالة إلى الأساسي" أنا عامل (44462)، وصلتني رسالة من الأساسي: "أرسل رسالة إلى العامل"
باستخدام هذه الآلية، يمكننا مراقبة حالة كل عملية فرعية بحيث عند حدوث حادث في عملية فرعية، يمكننا التدخل فيها في الوقت المناسب للتأكد من توفر الخدمات.
واجهة وحدة cluster بسيطة للغاية. من أجل توفير المساحة، نقوم هنا فقط بإصدار بعض البيانات الخاصة حول طريقة cluster.setupPrimary . بالنسبة للطرق الأخرى، يرجى التحقق من الوثائق الرسمية:
cluster.setupPrimary ، قم بالإعدادات ذات الصلةcluster.fork cluster.setupPrimarycluster.settings كل مكالمة إلى قيمة سمة cluster.settings الحالية؛cluster.setupPrimary ، لا يؤثر ذلك على عمليات المرور اللاحقة cluster.fork envcluster.setupPrimary إلا في العملية الرئيسية.قدمنا وحدة cluster سابقًا، والتي من خلالها يمكننا إنشاء مجموعة عمليات Node.js لتحسين كفاءة تشغيل البرنامج، ومع ذلك، تعتمد cluster على نموذج متعدد العمليات، مع تبديل عالي التكلفة بين العمليات والعزل يمكن أن تؤدي الزيادة في عدد العمليات الفرعية بسهولة إلى مشكلة عدم القدرة على الاستجابة بسبب قيود موارد النظام. لحل مثل هذه المشاكل، توفر Node.js worker_threads فيما يلي نقدم باختصار استخدام هذه الوحدة من خلال أمثلة محددة:
// server.js
const http = require('http');
const { Worker } = require('worker_threads');
http.createServer((req, res) => {
const httpWorker = new Worker('./http_worker.js');
httpWorker.on('message', (result) => {
res.writeHead(200);
res.end(`${result}n`);
});
httpWorker.postMessage('توم');
}).listen(8000);
// http_worker.js
const {parentPort } = require('worker_threads');
parentPort.on('message', (name) => {
parentPort.postMessage(`Welcone ${name}!`);
}); يوضح المثال أعلاه الاستخدام البسيط لـ worker_threads . عند استخدام worker_threads ، عليك الانتباه إلى النقاط التالية:
قم بإنشاء نسخة Worker من خلال worker_threads.Worker ، حيث يمكن أن يكون البرنامج النصي Worker إما ملف JavaScript مستقل أو字符串على سبيل المثال، يمكن تعديل المثال أعلاه على النحو التالي:
const code = "const {parentPort } = require('worker_threads');parentPort.on('message', (name) => {parentPort.postMessage(`Welcone ${ name}!` );})";
const httpWorker = new Worker(code, { eval: true });عند إنشاء نسخة عامل من خلال worker_threads.Worker ، يمكنك تعيين البيانات التعريفية الأولية لمؤشر ترابط العامل الفرعي عن طريق تحديد قيمة workerData ، مثل:
// server .js
const { Worker } = require('worker_threads');
const httpWorker = new Worker('./http_worker.js', {workerData: { name: 'Tom'} });
// http_worker.js
const {workerData } = require('worker_threads');
console.log(workerData);عند إنشاء نسخة عامل من خلال worker_threads.Worker ، يمكنك تعيين SHARE_ENV لإدراك الحاجة إلى مشاركة متغيرات البيئة بين مؤشر ترابط العامل الفرعي والخيط الرئيسي، على سبيل المثال:
const { Worker, SHARE_ENV } = تتطلب ("worker_threads")؛
const عامل = عامل جديد('process.env.SET_IN_WORKER = "foo"', { eval: true, env: SHARE_ENV });
عامل.ون('خروج', () => {
console.log(process.env.SET_IN_WORKER);
});بشكل مختلف عن آلية الاتصال بين العمليات في cluster ، يستخدم worker_threads قناة الرسائل للتواصل بين سلاسل الرسائل:
parentPort.postMessage ، ويعالج الرسائل من message الترابط الرئيسي من خلال الاستماع إلىmessage الخاص برسالة parentPorthttpWorker من خلال طريقة postMessage لمثيل خيط العامل الفرعي (هنا httpWorker ، ويتم استبداله بخيط العامل الفرعي أدناه)، ويعالج الرسائل من خيط العامل الفرعي من خلال الاستماع إلى حدث message الخاص بـ httpWorker .في Node.js، سواء كانت عملية فرعية تم إنشاؤها بواسطة cluster أو سلسلة عمليات فرعية تم إنشاؤها بواسطة worker_threads ، فإن جميعها لديها مثيل V8 وحلقة حدث خاصة بها. والفرق هو أن
على الرغم من أنه يبدو أن سلاسل العمليات الفرعية أكثر كفاءة من العمليات الفرعية، إلا أن سلاسل العمليات الفرعية لها أيضًا عيوب، أي أن cluster توفر موازنة التحميل، بينما تتطلب worker_threads منا إكمال تصميم وتنفيذ موازنة التحميل بأنفسنا.
تقدم هذه المقالة استخدام الوحدات الثلاث child_process ، cluster ، و worker_threads في Node.js، ومن خلال هذه الوحدات الثلاث، يمكننا الاستفادة الكاملة من مزايا وحدات المعالجة المركزية متعددة النواة وحل بعض المشكلات الخاصة بكفاءة في سلاسل العمليات المتعددة ( وضع الخيط) كفاءة تشغيل المهام (مثل الذكاء الاصطناعي ومعالجة الصور وما إلى ذلك). تحتوي كل وحدة على سيناريوهات قابلة للتطبيق. تشرح هذه المقالة فقط كيفية استخدامها بكفاءة بناءً على المشكلات التي تواجهك والتي لا تزال بحاجة إلى استكشافها بنفسك. أخيرًا، إذا كانت هناك أي أخطاء في هذه المقالة، آمل أن تتمكن من تصحيحها وأتمنى لكم جميعًا برمجة سعيدة كل يوم.