عمليات Node.js الفرعية: كل ما تحتاج إلى معرفته
كيفية استخدام spawn () و exec () و execFile () و fork ()
تحديث: هذه المقالة هي الآن جزء من كتابي "Node.js Beyond The Basics".اقرأ النسخة المحدثة من هذا المحتوى والمزيد حول Node على jscomplete.com/node-beyond-basics .
يعمل الأداء أحادي الخيط وغير المحظور في Node.js بشكل رائع لعملية واحدة. ولكن في النهاية ، لن تكون عملية واحدة في وحدة معالجة مركزية واحدة كافية للتعامل مع عبء العمل المتزايد لتطبيقك.
بغض النظر عن مدى قوة الخادم الخاص بك ، يمكن أن يدعم مؤشر ترابط واحد حمولة محدودة فقط.
حقيقة أن Node.js يعمل في خيط واحد لا يعني أنه لا يمكننا الاستفادة من عمليات متعددة ، وبالطبع ، آلات متعددة أيضًا.
يعد استخدام عمليات متعددة أفضل طريقة لتوسيع نطاق تطبيق Node. تم تصميم Node.js لبناء تطبيقات موزعة مع العديد من العقد. هذا هو سبب تسميته Node . يتم استيعاب قابلية التوسع في النظام الأساسي وهي ليست شيئًا تبدأ التفكير فيه لاحقًا في عمر التطبيق.
هذه المقالة عبارة عن كتابة لجزء من دورة Pluralsight الخاصة بي حول Node.js. أنا أغطي محتوى مشابه بتنسيق الفيديو هناك.يرجى ملاحظة أن ستحتاج جيدة فهم من نود.جي إس الأحداث و تيارات قبل أن تقرأ هذا المقال. إذا لم تكن قد قمت بذلك بالفعل ، فإنني أوصيك بقراءة هاتين المقالتين الأخريين قبل قراءة هذا المقال:
فهم بنية Node.js المدفوعة بالحدث
تنفذ معظم كائنات Node - مثل طلبات HTTP والاستجابات والتدفق - وحدة EventEmitter حتى يتمكنوا من ...
تيارات: كل ما تريد أن تعرفه
تتمتع تدفقات Node.js بسمعة طيبة في العمل معها ، بل وصعوبة فهمها. حسنًا ، لدي أخبار جيدة ...
وحدة العمليات التابعة
يمكننا بسهولة تدوير عملية فرعية باستخدام child_process
وحدة Node ويمكن لهذه العمليات الفرعية التواصل بسهولة مع بعضها البعض باستخدام نظام المراسلة.
و child_process
حدة تمكننا من الوصول إلى وظائف نظام التشغيل عن طريق تشغيل أي أمر نظام داخل، أيضا، عملية طفل.
يمكننا التحكم في دفق إدخال العملية الطفل ، والاستماع إلى تدفق الإخراج. يمكننا أيضًا التحكم في الحجج التي سيتم تمريرها إلى أمر نظام التشغيل الأساسي ، ويمكننا فعل ما نريد باستخدام إخراج هذا الأمر. يمكننا ، على سبيل المثال ، توجيه إخراج أحد الأوامر كمدخل إلى آخر (تمامًا كما نفعل في Linux) حيث يمكن تقديم جميع مدخلات ومخرجات هذه الأوامر إلينا باستخدام تدفقات Node.js.
لاحظ أن الأمثلة التي سأستخدمها في هذه المقالة كلها تستند إلى Linux. في Windows ، تحتاج إلى تبديل الأوامر التي أستخدمها مع بدائل Windows الخاصة بهم.
هناك أربع طرق مختلفة لخلق عملية طفل في عقدة: spawn()
، fork()
، exec()
، و execFile()
.
سنرى الاختلافات بين هذه الوظائف الأربع ومتى نستخدم كل منها.
ولدت العمليات التابعة
تقوم spawn
الوظيفة بتشغيل أمر في عملية جديدة ويمكننا استخدامها لتمرير هذا الأمر أي وسيطات. على سبيل المثال ، إليك رمز لإنشاء عملية جديدة من شأنها تنفيذ pwd
الأمر.
const { spawn } = require('child_process'); const child = spawn('pwd');
نحن ببساطة ندمر spawn
الوظيفة خارج child_process
الوحدة النمطية وننفذها باستخدام أمر نظام التشغيل كوسيطة أولى.
نتيجة تنفيذ spawn
الوظيفة ( child
الكائن أعلاه) هو ChildProcess
مثيل يقوم بتنفيذ EventEmitter API. هذا يعني أنه يمكننا تسجيل معالجات للأحداث على هذا الكائن الفرعي مباشرة. على سبيل المثال ، يمكننا أن نفعل شيئًا عندما تنتهي العملية الفرعية عن طريق تسجيل معالج exit
للحدث:
child.on('exit', function (code, signal) { console.log('child process exited with ' + `code ${code} and signal ${signal}`); });
يعطينا المعالج أعلاه مخرجًا code
للعملية الفرعية و signal
، إن وجد ، الذي تم استخدامه لإنهاء العملية الفرعية. يكون هذا signal
المتغير فارغًا عند إنهاء العملية الفرعية بشكل طبيعي.
الأحداث الأخرى التي يمكننا تسجيل معالجات للمع ChildProcess
حالات هي disconnect
، error
، close
، و message
.
- و
disconnect
ينبعث حدث عند عملية الأصل يدعو يدوياchild.disconnect
وظيفة. - يتم إصدار
error
الحدث إذا تعذر إنتاج العملية أو قتلها. - يتم إصدار
close
الحدث عندstdio
إغلاق تدفقات عملية فرعية. - و
message
الحدث هو الأكثر واحد مهم. تنبعث عندما تستخدم العملية الفرعيةprocess.send()
الوظيفة لإرسال الرسائل. هذه هي الطريقة التي يمكن بها لعمليات الوالدين / الأطفال التواصل مع بعضها البعض. سنرى مثالاً على ذلك أدناه.
كما يحصل كل عملية طفل القياسية ثلاثة stdio
تيارات، التي يمكننا الوصول باستخدام child.stdin
، child.stdout
و child.stderr
.
عندما يتم إغلاق هذه التدفقات ، فإن العملية الفرعية التي كانت تستخدمها ستصدر close
الحدث. close
يختلف هذا الحدث عن exit
الحدث نظرًا لأن عمليات فرعية متعددة قد تشترك في نفس stdio
التدفقات وبالتالي فإن إنهاء عملية فرعية واحدة لا يعني أن التدفقات تم إغلاقها.
نظرًا لأن جميع التدفقات عبارة عن بواعث للأحداث ، يمكننا الاستماع إلى أحداث مختلفة على تلك stdio
التدفقات المرتبطة بكل عملية طفل. على عكس العملية العادية ، على الرغم من ذلك ، في عملية فرعية ، تكون التدفقات stdout
/ stderr
التدفقات تدفقات قابلة للقراءة بينما يكون stdin
الدفق قابلًا للكتابة. هذا هو في الأساس معكوس تلك الأنواع كما هو موجود في عملية رئيسية. الأحداث التي يمكننا استخدامها لتلك التدفقات هي الأحداث القياسية. الأهم من ذلك ، في التدفقات التي يمكن قراءتها ، يمكننا الاستماع إلى data
الحدث ، والذي سيكون له إخراج الأمر أو أي خطأ واجهته أثناء تنفيذ الأمر:
child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`child stderr:\n${data}`); });
سيقوم المعالِجان أعلاه بتسجيل كلتا الحالتين إلى العملية الرئيسية stdout
و stderr
. عندما ننفذ spawn
الوظيفة أعلاه ، pwd
تتم طباعة إخراج الأمر وتنتهي العملية الفرعية برمز 0
، مما يعني عدم حدوث خطأ.
يمكننا تمرير الوسيطات إلى الأمر الذي تنفذه spawn
الوظيفة باستخدام الوسيط الثاني spawn
للدالة ، وهو عبارة عن مصفوفة من جميع الوسائط التي يتم تمريرها إلى الأمر. على سبيل المثال ، لتنفيذ find
الأمر في الدليل الحالي باستخدام -type f
وسيط (لسرد الملفات فقط) ، يمكننا القيام بما يلي:
const child = spawn('find', ['.', '-type', 'f']);
إذا حدث خطأ أثناء تنفيذ الأمر ، على سبيل المثال ، إذا قدمنا وجهة غير صالحة أعلاه ، child.stderr
data
فسيتم تشغيل exit
معالج الحدث وسيقوم معالج الحدث بالإبلاغ عن رمز الخروج 1
، مما يدل على حدوث خطأ. تعتمد قيم الخطأ فعليًا على نظام التشغيل المضيف ونوع الخطأ.
العملية الفرعية stdin
هي دفق قابل للكتابة. يمكننا استخدامه لإرسال بعض المدخلات للأمر. تمامًا مثل أي دفق قابل للكتابة ، فإن أسهل طريقة لاستهلاكه هي استخدام pipe
الوظيفة. نحن ببساطة نوجه دفقًا يمكن قراءته إلى تيار قابل للكتابة. نظرًا لأن العملية الرئيسية stdin
عبارة عن دفق قابل للقراءة ، يمكننا توجيه ذلك إلى stdin
دفق عملية تابع . فمثلا:
const { spawn } = require('child_process'); const child = spawn('wc'); process.stdin.pipe(child.stdin) child.stdout.on('data', (data) => { console.log(`child stdout:\n${data}`); });
في المثال أعلاه ، تستدعي العملية الفرعية wc
الأمر الذي يحسب الأسطر والكلمات والأحرف في Linux. نقوم بعد ذلك بتوجيه العملية الرئيسية stdin
(وهي دفق قابل للقراءة) في العملية الفرعية stdin
(وهي دفق قابل للكتابة). نتيجة هذه المجموعة هي أننا نحصل على وضع إدخال قياسي حيث يمكننا كتابة شيء ما وعندما نضغط Ctrl+D
، سيتم استخدام ما كتبناه كمدخل wc
للأمر.

يمكننا أيضًا توجيه المدخلات / المخرجات القياسية لعمليات متعددة على بعضها البعض ، تمامًا كما نفعل مع أوامر Linux. على سبيل المثال، يمكننا الأنابيب و stdout
من find
الأمر إلى ستدين من wc
الأوامر لحساب كافة الملفات في الدليل الحالي:
const { spawn } = require('child_process'); const find = spawn('find', ['.', '-type', 'f']); const wc = spawn('wc', ['-l']); find.stdout.pipe(wc.stdin); wc.stdout.on('data', (data) => { console.log(`Number of files ${data}`); });
I added the -l
argument to the wc
command to make it count only the lines. When executed, the code above will output a count of all files in all directories under the current one.
Shell Syntax and the exec function
By default, the spawn
function does not create a shell to execute the command we pass into it. This makes it slightly more efficient than the exec
function, which does create a shell. The exec
function has one other major difference. It buffers the command’s generated output and passes the whole output value to a callback function (instead of using streams, which is what spawn
does).
Here’s the previous find | wc
example implemented with an exec
function.
const { exec } = require('child_process'); exec('find . -type f | wc -l', (err, stdout, stderr) => { if (err) { console.error(`exec error: ${err}`); return; } console.log(`Number of files ${stdout}`); });
Since the exec
function uses a shell to execute the command, we can use the shell syntax directly here making use of the shell pipe feature.
Note that using the shell syntax comes at a security risk if you’re executing any kind of dynamic input provided externally. A user can simply do a command injection attack using shell syntax characters like ; and $ (for example, command + ’; rm -rf ~’
)
The exec
function buffers the output and passes it to the callback function (the second argument to exec
) as the stdout
argument there. This stdout
argument is the command’s output that we want to print out.
The exec
function is a good choice if you need to use the shell syntax and if the size of the data expected from the command is small. (Remember, exec
will buffer the whole data in memory before returning it.)
The spawn
function is a much better choice when the size of the data expected from the command is large, because that data will be streamed with the standard IO objects.
We can make the spawned child process inherit the standard IO objects of its parents if we want to, but also, more importantly, we can make the spawn
function use the shell syntax as well. Here’s the same find | wc
command implemented with the spawn
function:
const child = spawn('find . -type f | wc -l', { stdio: 'inherit', shell: true });
Because of the stdio: 'inherit'
option above, when we execute the code, the child process inherits the main process stdin
, stdout
, and stderr
. This causes the child process data events handlers to be triggered on the main process.stdout
stream, making the script output the result right away.
Because of the shell: true
option above, we were able to use the shell syntax in the passed command, just like we did with exec
. But with this code, we still get the advantage of the streaming of data that the spawn
function gives us. This is really the best of both worlds.
There are a few other good options we can use in the last argument to the child_process
functions besides shell
and stdio
. We can, for example, use the cwd
option to change the working directory of the script. For example, here’s the same count-all-files example done with a spawn
function using a shell and with a working directory set to my Downloads folder. The cwd
option here will make the script count all files I have in ~/Downloads
:
const child = spawn('find . -type f | wc -l', { stdio: 'inherit', shell: true, cwd: '/Users/samer/Downloads' });
Another option we can use is the env
option to specify the environment variables that will be visible to the new child process. The default for this option is process.env
which gives any command access to the current process environment. If we want to override that behavior, we can simply pass an empty object as the env
option or new values there to be considered as the only environment variables:
const child = spawn('echo $ANSWER', { stdio: 'inherit', shell: true, env: { ANSWER: 42 }, });
The echo command above does not have access to the parent process’s environment variables. It can’t, for example, access $HOME
, but it can access $ANSWER
because it was passed as a custom environment variable through the env
option.
One last important child process option to explain here is the detached
option, which makes the child process run independently of its parent process.
Assuming we have a file timer.js
that keeps the event loop busy:
setTimeout(() => { // keep the event loop busy }, 20000);
We can execute it in the background using the detached
option:
const { spawn } = require('child_process'); const child = spawn('node', ['timer.js'], { detached: true, stdio: 'ignore' }); child.unref();
The exact behavior of detached child processes depends on the OS. On Windows, the detached child process will have its own console window while on Linux the detached child process will be made the leader of a new process group and session.
If the unref
function is called on the detached process, the parent process can exit independently of the child. This can be useful if the child is executing a long-running process, but to keep it running in the background the child’s stdio
configurations also have to be independent of the parent.
The example above will run a node script (timer.js
) in the background by detaching and also ignoring its parent stdio
file descriptors so that the parent can terminate while the child keeps running in the background.

The execFile function
If you need to execute a file without using a shell, the execFile
function is what you need. It behaves exactly like the exec
function, but does not use a shell, which makes it a bit more efficient. On Windows, some files cannot be executed on their own, like .bat
or .cmd
files. Those files cannot be executed with execFile
and either exec
or spawn
with shell set to true is required to execute them.
The *Sync function
The functions spawn
, exec
, and execFile
from the child_process
module also have synchronous blocking versions that will wait until the child process exits.
const { spawnSync, execSync, execFileSync, } = require('child_process');
Those synchronous versions are potentially useful when trying to simplify scripting tasks or any startup processing tasks, but they should be avoided otherwise.
The fork() function
The fork
function is a variation of the spawn
function for spawning node processes. The biggest difference between spawn
and fork
is that a communication channel is established to the child process when using fork
, so we can use the send
function on the forked process along with the global process
object itself to exchange messages between the parent and forked processes. We do this through the EventEmitter
module interface. Here’s an example:
The parent file, parent.js
:
const { fork } = require('child_process'); const forked = fork('child.js'); forked.on('message', (msg) => { console.log('Message from child', msg); }); forked.send({ hello: 'world' });
The child file, child.js
:
process.on('message', (msg) => { console.log('Message from parent:', msg); }); let counter = 0; setInterval(() => { process.send({ counter: counter++ }); }, 1000);
In the parent file above, we fork child.js
(which will execute the file with the node
command) and then we listen for the message
event. The message
event will be emitted whenever the child uses process.send
, which we’re doing every second.
To pass down messages from the parent to the child, we can execute the send
function on the forked object itself, and then, in the child script, we can listen to the message
event on the global process
object.
When executing the parent.js
file above, it’ll first send down the { hello: 'world' }
object to be printed by the forked child process and then the forked child process will send an incremented counter value every second to be printed by the parent process.

Let’s do a more practical example about the fork
function.
Let’s say we have an http server that handles two endpoints. One of these endpoints (/compute
below) is computationally expensive and will take a few seconds to complete. We can use a long for loop to simulate that:
const http = require('http'); const longComputation = () => { let sum = 0; for (let i = 0; i { if (req.url === '/compute') { const sum = longComputation(); return res.end(`Sum is ${sum}`); } else { res.end('Ok') } }); server.listen(3000);
This program has a big problem; when the the /compute
endpoint is requested, the server will not be able to handle any other requests because the event loop is busy with the long for loop operation.
There are a few ways with which we can solve this problem depending on the nature of the long operation but one solution that works for all operations is to just move the computational operation into another process using fork
.
We first move the whole longComputation
function into its own file and make it invoke that function when instructed via a message from the main process:
In a new compute.js
file:
const longComputation = () => { let sum = 0; for (let i = 0; i { const sum = longComputation(); process.send(sum); });
Now, instead of doing the long operation in the main process event loop, we can fork
the compute.js
file and use the messages interface to communicate messages between the server and the forked process.
const http = require('http'); const { fork } = require('child_process'); const server = http.createServer(); server.on('request', (req, res) => { if (req.url === '/compute') { const compute = fork('compute.js'); compute.send('start'); compute.on('message', sum => { res.end(`Sum is ${sum}`); }); } else { res.end('Ok') } }); server.listen(3000);
When a request to /compute
happens now with the above code, we simply send a message to the forked process to start executing the long operation. The main process’s event loop will not be blocked.
Once the forked process is done with that long operation, it can send its result back to the parent process using process.send
.
في عملية الوالدين ، نستمع إلى message
الحدث على عملية الطفل المتشعبة نفسها. عندما نحصل على هذا الحدث ، sum
ستكون لدينا قيمة جاهزة لإرسالها إلى المستخدم الطالب عبر http.
الكود أعلاه ، بالطبع ، مقيد بعدد العمليات التي يمكننا تفرعها ، ولكن عندما ننفذها ونطلب نقطة نهاية الحساب الطويلة عبر http ، لا يتم حظر الخادم الرئيسي على الإطلاق ويمكن أن يأخذ المزيد من الطلبات.
تعتمد cluster
وحدة Node ، التي هي موضوع مقالتي التالية ، على فكرة تفرع العمليات الفرعية وموازنة التحميل بين الطلبات العديدة التي يمكننا إنشاؤها على أي نظام.
هذا كل ما لدي لهذا الموضوع. شكرا للقراءة! حتى المرة القادمة!
تعلم React أو Node؟ تحقق من كتبي:
- تعلم React.js عن طريق إنشاء الألعاب
- Node.js ما وراء الأساسيات