Back

Concurrency vs. Parallelism in JavaScript

Concurrency vs. Parallelism in JavaScript

In the evolving world of web development, JavaScript stands as a cornerstone, powering interactive and dynamic user experiences. This article explores two pivotal concepts, Concurrency and Parallelism, which are integral to enhancing the performance and efficiency of web applications, thereby optimizing system operations.

Concurrency and parallelism are crucial, as they enable the simultaneous execution of multiple tasks, significantly boosting system throughput and efficiency, leading to more efficient use of resources and faster completion of complex, time-sensitive operations. They are the backbone of modern computing, enabling high-performance applications that serve numerous users and process vast amounts of data. This article provides a comprehensive guide on harnessing concurrency and parallelism to elevate system performance by exploring their definitions, distinctions, pros, and cons, alongside a selection of practical examples that illustrate the implementation of these concepts in managing concurrent operations.

What is Concurrency in JavaScript?

Concurrency refers to the ability to manage and execute multiple tasks asynchronously, allowing a program to utilize time while waiting for other tasks to complete. Since JavaScript is single-threaded, concurrency gives way to handling tasks such as events and I/O operations in a non-blocking manner, ensuring that the main thread is free to continue processing user interactions and other tasks.

It is simply a way to deal with multiple tasks at once. A real-world instance would be a chef managing several dishes simultaneously by switching to each that needs attention. This way, you can have several tasks start, run, and complete in overlapping periods.

Examples of Concurrency in JavaScript

Various strategies exist for executing tasks concurrently, each focused on enhancing system performance through efficient task management.

Some of these techniques are:

  • Callbacks
  • Promises

Callbacks

Callbacks are functions utilized as parameters within other functions, allowing for asynchronous task execution. This mechanism facilitates the handling of concurrent operations, ensuring that other processes can proceed uninterrupted while awaiting the completion of a specific task.

An example provided below demonstrates the effective use of callbacks to achieve concurrency.

function fetchData(callback) {
  // Simulate a delay to mimic data fetching, like waiting for a server response.
  setTimeout(() => {
    // Once the 'data' is ready, the callback function is called with the 'data' as an argument.
    callback('Data fetched');
  }, 1000); // The delay is set to 1000 milliseconds (1 second).
}

// Specify post-data-fetch actions
fetchData((data) => {
  // Function to run after data is retrieved
  console.log(data); // Display: 'Data fetched'
});

In the example above, fetchData is a function that simulates fetching data from a remote source, which takes time (asynchronously). We use setTimeout to mimic this delay. The callback is a predefined function that automatically executes once the data retrieval process is complete.

Promises

A promise is an object that encapsulates the eventual outcome of asynchronous operations, offering a structured and more efficient approach to managing such tasks than traditional callbacks.

Below is a simple code that uses promises to achieve concurrency.

function fetchData() {
  // fetchData immediately returns a Promise.
  return new Promise((resolve) => {
    // Inside the Promise, we again simulate a delay in fetching data.
    setTimeout(() => {
      // Once the data is ready, we 'resolve' the Promise with the data.
      resolve('Data fetched');
    }, 1000); // The delay is 1000 milliseconds (1 second).
  });
}

// fetchData returns a Promise upon call
fetchData().then(data => {
  // Use .then() to handle the Promise resolution
  console.log(data); // Console output: 'Data fetched'
});

In this snippet, fetchData initiates an asynchronous operation that returns a Promise. The .then() method handles the outcome by logging the resolved data to the console. This approach ensures a clear and consistent flow for managing asynchronous data retrieval.

Pros of Concurrency

  • Optimal Resource Allocation: Concurrency maximizes resource utilization by handling I/O-bound tasks effectively.
  • Responsiveness: It improves user experience by not blocking the main thread.
  • Scalability: Facilitates handling a high number of operations simultaneously.

Cons of Concurrency

  • Complexity: Concurrency can introduce challenges in managing state and coordinating tasks.
  • Debugging Difficulty: Tracing and debugging asynchronous codes can be difficult.
  • Error-prone Dynamics: Proper management is crucial to prevent race conditions and deadlocks.

What is Parallelism in JavaScript?

Parallelism allows for the simultaneous execution of multiple tasks across several threads, which differs from concurrency, which manages task execution in an overlapping manner. It is like having multiple tabs opened in your browser. Each tab operates independently and can perform tasks without waiting for the others to finish. In other words, concurrency deals with managing lots of things at once by handling each task one at a time, while parallelism typically involves doing several tasks at once.

Examples of Parallelism in Javascript

Since JavaScript is single-threaded, the following methods can produce true parallelism:

Web Workers

Web Workers allow you to run JavaScript in background threads, separate from the main execution thread of a web application. With Web Workers, you can perform tasks in parallel without interrupting the user interface.

Below is a simple code that illustrates the use of Web Workers.

// main.js
// Determine the number of available CPU cores
const cores = navigator.hardwareConcurrency || 4; // Fallback to 4 if hardwareConcurrency is not supported

// Function to initialize and start workers
function startWorkers() {
  for (let i = 0; i < cores; i++) {
    const worker = new Worker(`worker${i}.js`);
    worker.postMessage(`Start working on task ${i}`);

    worker.onmessage = function(event) {
      console.log(`Worker ${i} says:`, event.data);
    };
  }
}

// Start the workers
startWorkers();

For this example, you would create multiple worker files (e.g., worker0.js, worker1.js, etc.), each containing a task that simulates a heavy computation:

// worker0.js, worker1.js, ..., workerN.js
self.onmessage = function(event) {
  console.log(event.data); // Log the received message
  const result = performHeavyComputation();
  self.postMessage(result);
};

function performHeavyComputation() {
  // Simulate a heavy computation task
  let sum = 0;
  for (let i = 0; i < 1e8; i++) {
    sum += Math.sqrt(i);
  }
  return `Task completed with the result: ${sum}`;
}

The code will generate console log messages in both threads. In the main thread, a log message will confirm the activation of a worker. In the worker threads, messages will signal the receipt of the start command and the completion of the intensive computation task.

Here’s an example of what the console output might look like:

Worker 0 says: Task completed with the result: [sum from worker 0]
Worker 1 says: Task completed with the result: [sum from worker 1]
...
Worker N says: Task completed with the result: [sum from worker N]

Each [sum from worker X] represents the sum of the square roots of the numbers from 0 to ( 1e8 ) calculated by each worker. The sum would depend on the computation performed by each worker file’s performHeavyComputation function.

When a system has multiple CPU cores, Web Workers utilize them for parallel processing. Each worker operates on an individual core, achieving true parallelism and enabling tasks like complex calculations to run simultaneously, thus enhancing efficiency.

However, if the number of workers exceeds the number of CPU cores available, the browser will manage their execution. While they may still run concurrently, they won’t operate in true parallelism. This scenario can lead to performance issues due to the need for context switching and the additional overhead associated with managing multiple workers on fewer cores.

For example, in a system with eight CPU cores, creating eight worker threads can allow for tasks to be executed concurrently, potentially on separate cores. However, it is important to remember that the main thread, which creates the workers, also requires CPU time. Therefore, in practice, one might create fewer than eight workers to ensure the main thread can operate efficiently without resource contention.

It is also worth knowing that while it is technically feasible to create a large number of Web Workers, such as 100, it’s generally not advised because it can lead to performance problems. Each web worker runs in its thread, and having too many can lead to high memory usage and increased complexity in managing them. It is better to balance the need for parallel processing with the available system resources and the overhead of managing multiple workers.

Parallel.js

Parallel.js is a library that provides an abstraction over Web Workers to perform operations in parallel. It provides an easier way to perform parallel processing by abstracting Web Workers, making it easier to run functions in parallel. To use this library, you would typically import it and then use its API to run tasks in parallel.

Below is an example of its usage to achieve parallelism.

// Importing parallel.js
const Parallel = require('paralleljs');
// Create a new Parallel object with an array of data.
const p = new Parallel([1, 2, 3, 4]);

// Use the .map() method to operate on each element in parallel.
p.map(number => number * 2).then(data => {
  // Once the parallel operation is complete, .then() is called with the results.
  console.log(data); // Expected output: [2, 4, 6, 8]
});

In the example above, we create a Parallel object and assign it to an array of numbers. We then double each number using the .map() method. The operation executes in parallel, and the results are displayed once completed.

Web Workers and Parallel.js handle computationally intensive tasks without blocking the main thread, thus keeping the web application responsive. Web Workers require you to manage the separate threads and communication yourself, while the library offers a higher-level API to abstract some of this complexity. Each has its use cases depending on the complexity of the task and the level of control you need over the parallel execution.

Pros of Parallelism

  • Performance: Parallelism can significantly speed up CPU-intensive tasks.
  • Non-blocking: Operations don’t block the main thread, maintaining application responsiveness.
  • Resource Utilization: It makes use of multi-core processors effectively.

Cons of Parallelism

  • Resource consumption: Running tasks in parallel can consume more system resources, such as CPU and memory, especially when dealing with numerous concurrent tasks.
  • Browser Support: Modern browsers generally provide functionality for Web Workers, allowing JavaScript to run in background threads. However, not all browsers support it. It is crucial to check browser compatibility to ensure functionality across different user environments. For the most current browser support information for Web Workers, refer to the support tables provided by the “Can I use…” site. This resource offers comprehensive and up-to-date details on browser compatibility for a wide range of web features.
  • Debugging and Testing: Debugging parallel codes can be more challenging than sequential code.

Conclusion

Concurrency and Parallelism are core concepts in JavaScript that are necessary to ensure the system’s efficiency and performance. Although they both have disadvantages, focusing on their advantages will benefit the development of efficient web applications.

Remember, concurrency is the management of multiple tasks by executing them one at a time, often overlapping, while parallelism is executing multiple tasks simultaneously. While parallelism aims to maximize performance by leveraging hardware resources efficiently, concurrency is more about managing the complexity of tasks in a multi-tasking manner.

JavaScript, being a single-threaded language, achieves parallelism using techniques like Web Workers and Parallel.js library, as discussed earlier in this article. Concurrency, on the other hand, is handled through asynchronous programming models like callbacks and promises. Effective utilization of both concepts can lead to the development of responsible JavaScript applications.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay