Tuesday, May 28, 2024

The Truth About Node.js: Single-Threaded or Multi-Threaded?

Node.js is often described as single-threaded, which can be somewhat misleading. This description primarily refers to the JavaScript execution model. Node.js uses an event-driven, non-blocking I/O model, which allows it to handle many concurrent connections efficiently with a single main thread. However, this doesn't mean that Node.js can't take advantage of multiple threads. Under the hood, Node.js uses a thread pool for certain operations, and with the introduction of worker threads, Node.js can execute JavaScript code in multiple threads.

NOTE: I have described it very clearly in my below Youtube video. The link of the video is https://youtu.be/8IDW3OQ_blQ?si=XCmM3x45nt1FRgj-


Understanding the Single-Threaded Nature

Node.js runs JavaScript code in a single thread. This means that all JavaScript code execution happens in a single call stack, and operations are executed one at a time. This is efficient for I/O-bound tasks, where the main thread can offload tasks like file I/O or network requests to the system's underlying non-blocking capabilities and continue executing other JavaScript code.

Below is the example code where I have created two routes:

const express = require('express');
const app = express();
const port = 3002;

app.get('/nonblocking', (req, res) => {
  res.send("This is non blocking api");
});
app.get('/blocking', (req, res) => {
    for(let i=0; i<99999999999999; i++) {    
    }
   res.send("This is blocking api");
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});


Here, the route '/nonblocking' is very simple and light route where I am just returning a string in response and it is taking just few millisecond to return the response. But, in the 2nd route '/blocking', I am traversing a very big loop that is taking several minutes. Now, if any user is calling this route, then the main thread gets blocked in processing this request and it is not available to other request and thus even "/nonblocking" routes are also waiting until the "/blocking" routes gets processed completely. To solve, this issue, I have used worker thread in my below code.

Example of Worker Threads

App.js: Here, I am creating a thread:

const express = require('express');
const app = express();
const port = 3002;

app.get('/nonblocking', (req, res) => {
  res.send("This is non blocking api");
});
app.get('/blocking', (req, res) => {
  const { Worker } = require('worker_threads');
  const worker = new Worker('./worker.js',  {workerData: 999999} );
    worker.on('message', (data) => {
      res.send("This is blocking api with data = "+data);
    });
    worker.on('error', (error) => {
      console.log(error);
    });
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    })  
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});


Here, the line const worker = new Worker('./worker.js',  {workerData: 999999} );

is basically creating a new thread. This thread is emitting events like message, error and exit. We should listen these event to process the data accordingly.

worker.js : Here, we are basically, defining the logic of the task that this thread must perform and once task is completed then it will publish the resultant data by parentPort.postMessage(data);

const { parentPort, workerData } = require('worker_threads');

function countTo(number) {
    let count=0;
    for(let i=0; i<number; i++) {
        count = count + i;
    }
    return count;
}

const result = countTo(workerData);

parentPort.postMessage(result);

Pros and Cons of Using Worker Threads Pros:

  • Improved Performance for CPU-bound Tasks: Worker threads can significantly improve the performance of CPU-bound tasks by offloading them to separate threads.
  • Parallel Execution: Allows for true parallel execution of JavaScript code, which can lead to more efficient use of multi-core processors.
  • Enhanced Application Responsiveness: By handling heavy computations in worker threads, the main thread remains responsive to I/O operations and user interactions.

Cons:

  • Complexity: Introducing multi-threading can add complexity to the codebase, making it harder to understand and maintain.
  • Shared Memory Concerns: While worker threads can share memory using SharedArrayBuffer, managing shared memory can be tricky and prone to errors.
  • Overhead: Creating and managing worker threads introduces some overhead, which might not be justified for small or simple tasks.
When to Use Worker Threads in Node.js

Worker threads are particularly useful in the following scenarios:
  • CPU-bound Tasks: Any task that requires significant CPU time, such as complex calculations, image processing, or data parsing, can benefit from being executed in worker threads.
  • Parallel Processing: Tasks that can be divided into smaller, independent sub-tasks and executed in parallel will see performance gains.
  • Offloading Intensive Tasks: Tasks that could block the event loop, such as large file processing, can be offloaded to worker threads to keep the main thread responsive.
  • Real-time Applications: Applications that require real-time processing and cannot afford to have the main thread blocked will benefit from using worker threads.

Conclusion

Node.js is fundamentally single-threaded for JavaScript execution, but it can utilize multiple threads for certain operations and with the introduction of worker threads. This dual capability allows developers to handle both I/O-bound and CPU-bound tasks efficiently. By understanding when and how to use worker threads, developers can optimize their Node.js applications for better performance and responsiveness.





No comments:

Post a Comment

Please provide your precious comments and suggestion