Skip to content

Instantly share code, notes, and snippets.

@luismramirezr
Created May 19, 2023 19:40
Show Gist options
  • Save luismramirezr/361700f399cda6efa770261b36870509 to your computer and use it in GitHub Desktop.
Save luismramirezr/361700f399cda6efa770261b36870509 to your computer and use it in GitHub Desktop.
Concurrency, Parallelism, and Multi-threading in Node.js

Concurrency, Parallelism, and Multi-threading in Node.js

In the world of computing, the terms "concurrency" and "parallelism" are often used interchangeably, but they represent distinct concepts with significant implications for optimizing performance and efficiency. To understand better how Node.js, with its single-threaded event loop, works with concurrency and parallelism, let's first explore the fundamental differences between concurrency and parallelism and then dive into the event loop in Node.js, its role in implementing concurrency and parallelism, and how it differs from traditional multithreading approaches.

Concurrency: "Simultaneous" Execution

Concurrency refers to the ability of a computer system to execute multiple tasks concurrently. It involves breaking down a complex problem into smaller, independent tasks that can be executed in overlapping time intervals. While the tasks may appear to be executing simultaneously, they are actually taking turns running on a single processor or core. Concurrent operation means that two computations can both make progress and advance regardless of the other. If there are two threads, for example, then both make progress independently. The second computation doesn't need to wait for the first one to complete before it can be advanced.

Concurrency is essential for improving the responsiveness and efficiency of a system. By allowing tasks to be interleaved, the computer can make progress on different operations even if one task is temporarily blocked, such as waiting for input or accessing external resources. This enables efficient resource utilization and enhances the overall user experience.

Parallelism: Simultaneous Execution

Parallelism, on the other hand, takes concurrency to the next level by enabling true simultaneous execution of multiple tasks across multiple processors or cores. It involves breaking down a problem into smaller subproblems that can be solved independently and assigning each subproblem to a separate processor or core for simultaneous execution. Parallel operation means that two computations are literally running at the same time. At one point in time, both computations advance. There is no taking turns; they advance at the same time. Naturally this is not possible with single-core CPU, but multiple-core architecture is required instead.

Parallelism is particularly beneficial for computationally intensive tasks that can be divided into smaller, independent computations. By distributing the workload across multiple processors or cores, parallelism significantly reduces the time required to complete the task.

Node.js and its Event Loop

At the core of Node.js lies the event loop, a mechanism that allows asynchronous, non-blocking I/O operations. The event loop continuously monitors and processes events, such as incoming requests or data available for reading from a file. It efficiently manages these events, ensuring that the application remains highly responsive and performant. Therefore, at a high level, Node.js falls into the category of concurrent computation. This is a direct result of the single-threaded event loop being the backbone of a Node.js application. The event-loop repeatedly takes an event and then sequentially executes all listeners interested in that event. The event loop never runs two pieces of JavaScript in parallel.

As long as the event handlers are small and frequently wait for yet more events themselves, all computations (for example fulfilling and serving an HTTP request) can advance one small step at a time - concurrently. This is beneficial in web applications where the majority of the time is spent waiting for I/O to complete. It allows a single Node.js process to handle huge amounts of requests.

But what happens if we need to do synchronous-intense stuff, such as making complex calculations in memory in a large dataset? Then we might have a synchronous block of code that takes a lot of time and will block the rest of the code. Here it would be beneficial to have parallelism. While it's not possible to run two pieces of JavaScript at the same time that would have access to shared JS objects, there are few ways you can run isolated JavaScript computations in parallel. To achieve this, you can, starting from Node v10, launch a given piece of code in a worker thread and synchronize by passing copies of JS objects. It is easier understand if you see it as you're effectively launching multiple event-loops.

Parallelism and Worker Threads:

Web Workers are the way to achieve true multi-processing in Node.js, as setInterval(), setTimeOut(), XMLHttpRequest, async/await, and event handlers mimic parallelism, but in reality, it's just concurrency. A worker thread in Node.js is an actual OS thread running in a different instance of V8. To achieve true parallelism and execute CPU-intensive tasks across multiple cores, Node.js provides the worker_threads module. This module allows the creation of additional threads, called worker threads, alongside the main event loop. These worker threads operate independently and enable parallel execution of computationally intensive operations.

Some benefits of using multi-threading in Node.js include:

  1. Improved Performance: Multithreading allows Node.js applications to utilize multiple CPU cores effectively, enabling concurrent execution of CPU-intensive operations. By offloading these tasks to worker threads, the main event loop remains free to handle incoming requests, resulting in improved overall performance and reduced response times.

  2. Scalability: With multithreading, Node.js applications can handle a larger number of simultaneous requests and scale efficiently. By leveraging worker threads to process computationally intensive operations, the application can effectively distribute the workload across available CPU cores, accommodating increased user demand and maintaining responsiveness.

  3. Efficient Resource Utilization: Multithreading enables efficient utilization of system resources by parallelizing tasks across multiple threads. This approach optimizes CPU usage and prevents underutilization of available processing power. By effectively utilizing all available cores, Node.js applications can make the most of the underlying hardware.

It's important to note that not all applications can benefit from multithreading. The decision to use worker threads should be based on the nature of the application and the types of tasks it performs. Applications with heavy computational requirements, such as image processing, data crunching, or intensive algorithms, are ideal candidates for leveraging multithreading.

Let's take a look at a very simpple example to demonstrate the usage of multithreading in Node.js:

// main.js
const { Worker } = require('worker_threads');

// Create a pool of worker threads
const numWorkers = 2;
const workers = [];
for (let i = 0; i < numWorkers; i++) {
  workers.push(new Worker('./worker.js'));
}

// Send tasks to the worker threads
workers.forEach((worker, index) => {
  const task = `Task ${index + 1}`;
  
  // Listen for messages from the worker thread
  worker.on('message', (message) => {
    console.log(`Received result from worker ${index + 1}:`, message);
  });
  
  // Send a task to the worker thread
  worker.postMessage(task);
});
// worker.js
const { parentPort } = require('worker_threads');

// Listen for messages from the main thread
parentPort.on('message', (task) => {
  console.log(`Received task from main: ${task}`);
  
  // Simulate some heavy task processing
  const result = performTask(task);
  
  // Send the result back to the main thread
  parentPort.postMessage(result);
});

// Function to perform the task
function performTask(task) {
  // Simulate some time-consuming task
  for (let i = 0; i < 1000000000; i++) {
    // Do some computation
  }
  
  return `Result: ${task} completed`;
}

In this example, the main.js file creates a pool of worker threads by creating multiple instances of the Worker class. It sends tasks to the worker threads using the postMessage() method and listens for messages from each worker thread using the on('message') event handler. The worker.js file receives a task from the main thread, performs some time-consuming task simulation, and sends the result back to the main thread using the postMessage() method.

Conclusion

The event loop in Node.js provides a robust framework for implementing concurrency and managing asynchronous I/O operations within its single-threaded architecture. However, the introduction of worker threads in Node.js opens up new avenues for developing high-performance and scalable applications.By harnessing the power of parallel execution through multithreading, applications can utilize the capabilities of the hardware and effectively handle CPU-intensive tasks without sacrificing responsiveness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment