نعلم جميعًا أن 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.signal
encoding
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
false
uid
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
ignore
detached
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.setupPrimary
cluster.settings
كل مكالمة إلى قيمة سمة cluster.settings
الحالية؛cluster.setupPrimary
، لا يؤثر ذلك على عمليات المرور اللاحقة cluster.fork
env
cluster.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
الخاص برسالة parentPort
httpWorker
من خلال طريقة postMessage
لمثيل خيط العامل الفرعي (هنا httpWorker
، ويتم استبداله بخيط العامل الفرعي أدناه)، ويعالج الرسائل من خيط العامل الفرعي من خلال الاستماع إلى حدث message
الخاص بـ httpWorker
.في Node.js، سواء كانت عملية فرعية تم إنشاؤها بواسطة cluster
أو سلسلة عمليات فرعية تم إنشاؤها بواسطة worker_threads
، فإن جميعها لديها مثيل V8 وحلقة حدث خاصة بها. والفرق هو أن
على الرغم من أنه يبدو أن سلاسل العمليات الفرعية أكثر كفاءة من العمليات الفرعية، إلا أن سلاسل العمليات الفرعية لها أيضًا عيوب، أي أن cluster
توفر موازنة التحميل، بينما تتطلب worker_threads
منا إكمال تصميم وتنفيذ موازنة التحميل بأنفسنا.
تقدم هذه المقالة استخدام الوحدات الثلاث child_process
، cluster
، و worker_threads
في Node.js، ومن خلال هذه الوحدات الثلاث، يمكننا الاستفادة الكاملة من مزايا وحدات المعالجة المركزية متعددة النواة وحل بعض المشكلات الخاصة بكفاءة في سلاسل العمليات المتعددة ( وضع الخيط) كفاءة تشغيل المهام (مثل الذكاء الاصطناعي ومعالجة الصور وما إلى ذلك). تحتوي كل وحدة على سيناريوهات قابلة للتطبيق. تشرح هذه المقالة فقط كيفية استخدامها بكفاءة بناءً على المشكلات التي تواجهك والتي لا تزال بحاجة إلى استكشافها بنفسك. أخيرًا، إذا كانت هناك أي أخطاء في هذه المقالة، آمل أن تتمكن من تصحيحها وأتمنى لكم جميعًا برمجة سعيدة كل يوم.