For enquiries call:

Phone

+1-469-442-0620

Easter Sale-mobile

HomeBlogWeb DevelopmentHow Is a Node.JS Child Process Launched?

How Is a Node.JS Child Process Launched?

Published
05th Sep, 2023
Views
view count loader
Read it in
8 Mins
In this article
    How Is a Node.JS Child Process Launched?

    There is a high probability that you have already heard or read somewhere that Node.js is single-threaded. Although it is true, there is some nuance to this statement. In this article let’s quickly look at the threading architecture in Node.js, understand its limitations and also discuss four different techniques through which we could overcome those limitations.

    The Nuance with Node.js and Threads:

    JavaScript is a single-threaded language and since Node.js is more of an environment to run JavaScript outside of a browser, we could say that Node.js is also single-threaded.

    But, it is crucial to understand that while the code we write runs on a single thread with an event loop in the same thread, certain operations performed by some Node libraries that interact with the underlying operating system might use a multi-threaded approach.

    For instance, the crypto library in Node uses the libuv library internally, which is written in C, and libuv has access to a thread pool. Hence the operations performed by the crypto library may be executed in a multithreaded fashion.

    The Limitations With Node.js:

    Because our code runs on a single thread, CPU-intensive actions in our code logic will block the event thread; thereby increasing the server latency and reducing its throughput.

    An example of a CPU-intensive task could be parsing a huge Excel sheet, manipulating the data from it and writing the results to a new sheet.

    Typically, Node.js is not the preferred tool to do such jobs. But let’s say that we have some tech stack limitations and can only use Node. Then, we’ll have to find a way to make sure that this computationally heavy process doesn’t stall our thread.

    Some Workarounds to Tackle the Single-threaded Nature:

    The most common approach is to resort to multiprocessing using the cluster library in Node. A second common approach is a multi-threading approach using worker threads.

    In this article, we’ll be looking at various ways to resort to a multiprocessing approach. We won’t be discussing the cluster module but will be having a look at the fork method from the `child_process` module in Node, which the cluster module uses internally to achieve multiprocessing.

    The `child_process` module makes it possible to spawn subprocesses from the main process. All the spawned subprocesses, by default will have pipes established with the main process for the standard streams namely: `stdout`, `stdin` and `stderr`.

    The Various APIs for Multiprocessing in Node Child_process module:

    Node handles multiprocessing through a module named `child_process` and as the name suggests, it spawns child processes from the main process. The following are the four different ways to create child processes using the module:

    1. spawn()
    2. exec()
    3. execFile() and
    4. fork()

    We will briefly discuss how these four methods work and the differences between them.

    Spawn:

    Spawn is a function exported by the `child_process` module, so we could use it in our code in the following way:

    const {spawn} = require('child_process');   
    const lsCommand = spawn('ls');

    The spawn method, when invoked, creates a child process and runs the command which is passed as the first parameter to the spawn function in the created process. In the example above, the `ls` command which is used to list all files and directories on a Linux machine, will be run on the new child process.

    Optionally, spawn also accepts a second parameter which is an array of arguments to the passed command. By default, it will be an empty array.

    Let’s assume that we need to display the hidden files as well. In this case, we usually pass the `-a` switch to the `ls` command. We can do that through the spawn function as follows:

    const lsCommand = spawn('ls',['-a']);

    It is interesting to note that the new child process instance created by spawn implements the EventEmitter API and hence we can attach listeners for different events on various streams such as stdin , stdout and stderr.

    In the following code snippet, let’s look at how we could attach listeners to the data event on the stdout stream in a process created by the  spawn function:

    const {spawn} = require('child_process'); 
      
    const lsCommand = spawn('ls', ['-a']); 
      
    console.log(`main process id: ${process.pid}`); 
    console.log(`spawned child process id: ${lsCommand.pid}`); 
      
    lsCommand.stdout.on('data',(data)=>console.log(`Output from spawned child process with id ${lsCommand.pid}:\n ${data}`));

    The execution of the above snippet will be as follows:

    How Is a Node.JS Child Process Launched?

    Exec:

    The exec function also creates a child process. However, unlike the spawn function, it creates a shell first and then runs the command provided to it as the first argument.

    The first argument could be a command or a location to a script file that you would like to be executed in the spawned shell in the child process.

    We could use a callback as a second argument, that buffers the output of the executed command from the standard output (stdout) and the stand error (stderr) streams.

    Let’s look at a code snippet with exec():

    const { exec } = require('child_process'); 
      
    const execCallback = (error, stdout, stderr)=>{ 
      if (error) { 
        console.error(`exec error: ${error}`); 
        return; 
      } 
      console.log(`standard output: ${stdout}`); 
      console.log(`standard error: ${stderr}`); 
    } 
      
    const catCommand = exec('cat spawn-demo.js | wc -w', execCallback); 
    console.log(`Main Process Id: ${process.pid}`); 
    console.log(`child process Id: ${catCommand.pid}`);

    As we can see from the code snippet above, since the command we pass to exec will be run on a shell we could use shell pipes `|` to combine multiple commands. In the above code example, we can print the content of spawn-demo.js file and count the number of words in the output.

    How Is a Node.JS Child Process Launched?

    ExecFile:

    The execFile function differs from exec function in the way that it does not create a shell on which to run the supplied command. Hence, we could use this as an option if we would like to execute some background scripts without blocking our applications thread.

    Let’s assume that we have a demo bash script file named `versions.sh` as follows:

    # versions.sh 
    #!/bin/bash 
    
    echo "printing different software versions" 
    echo "node version:  $(node -v)" 
    echo "npm version: $(npm -v)" 
    echo "script run complete"

    Now, we can use the execFile function to run this script on a separate child process without creating a shell as follows:

    const {execFile} = require('child_process');
     
    const versionsScriptRunner = execFile('./versions.sh',(err,stdout)=>{
      if(err){
        console.log("script file failed to be executed");
        return;
      }
      console.log(stdout);
    });

    When we run the above snippet, the output will be as follows:

    How Is a Node.JS Child Process Launched?

    Fork:

    The fork function is very similar to the spawn function except for the difference that when fork is used, the created child process will have an IPC (Inter-Process Communication) channel established with the parent process. Thus, we could use this method for use cases where we require a bi-directional communication channel between the parent and the child processes. 

    Let’s have a timer file as follows:

    //timer.js  
      
      let seconds = 0; 
      
      setInterval(()=>{ 
       process.send({seconds:`${++seconds} form pid ${process.pid}`}); 
      },1000); 
      
      process.on('message',msg=>{ 
       console.log('from parent process',msg); 
      });

    Now, we’ll create a child process with the fork method and run the timer.js file on the forked process as follows:

    const { fork } = require('child_process'); 
      
    const forkedProcess = fork('timer.js'); 
      
    forkedProcess.on('message', (msg) => { 
      console.log('Message from child', msg); 
    }); 
      
    forkedProcess.send({ message: `This is from parent pid: ${process.pid}` })

    The above code snippets demonstrate the IPC channel between the parent and child processes:

    How Is a Node.JS Child Process Launched?
    The Comparison:

    CriteriaSpawnExecExecFileFork
    Creation of a new shell on child-process instantiationBy default, a shell is not spawned to execute the given command.By default, a shell is spawned and the given command is executed on it.Doesn’t spawn a shell by default to execute the command.Doesn’t spawn a shell.
    Option to spawn a shellCan be made to create a shell by passing the option shell: trueCan be made to avoid spawning a shell by passing the option shell: falseCan be made to create a shell by passing the option shell: trueDoes not support the shell option.
    EfficiencySince the default behavior doesn’t involve shell spawning, it is considered to be more efficient than exec.The default behavior is less efficient as a shell needs to be created first before running the given commands.The default behavior is more efficient than the exec method.Doesn’t involve shell spawning, but spawning a large number of child processes might lead to resource-hogging, as the spawned processes will have their own v8 instance.
    BehaviorAsynchronous AsynchronousAsynchronousAsynchronous
    Generated output handling of the executed commandOutput is streamedOutput is bufferedOutput is bufferedOutput is streamed
    Synchronous VersionspawnSync()execSync()execFileSync()NA

    Conclusion:

    In general, if one is going to deal with a resource-intensive task that might potentially block the main thread, it will be a good approach from a performance perspective to offload that task to a separate process to make sure that our main thread is not stalled,  thereby preventing our code from blocking the event loop.

    If your intention of creating a new process is just to scale the number of running instances of your application to serve increased traffic, looking into the `cluster` module will be helpful. 

    If you’d like to explore a multi-threaded approach rather than a multi-processing approach to deal with computationally intensive tasks in Node.js, you should have a look at another module named `worker_threads`.

    Profile

    Parthipan Natkunam

    Author

    Parthipan is a full-stack capable, software engineer with 7 years of industry experience, specialized in front-end engineering. Certified "AWS Developer Associate", who has worked in industries such as Ed-tech, IoT and Cyber security with companies spanning across multiple countries with varied tech stacks. Language agnostic by practice but fluent in TypeScript and JavaScript. Loves open-source and enjoys creating quality software products from scratch.

    Share This Article
    Ready to Master the Skills that Drive Your Career?

    Avail your free 1:1 mentorship session.

    Select
    Your Message (Optional)

    Upcoming Web Development Batches & Dates

    NameDateFeeKnow more
    Course advisor icon
    Course Advisor
    Whatsapp/Chat icon