"Optimizing CPU Utilization with Multithreading: Exploring JavaScript Worker Threads"

"Optimizing CPU Utilization with Multithreading: Exploring JavaScript Worker Threads"

Introduction

Threads are units of execution within a program that can run concurrently. They represent independent flows of control that can perform tasks simultaneously. In a multithreaded environment, a program can consist of multiple threads, each capable of executing its own sequence of instructions.

Multithreading is a programming technique that enables concurrent execution of multiple threads within a program. Threads are independent sequences of instructions that can run simultaneously, allowing tasks to be performed concurrently. This parallel execution can lead to improved performance, efficient resource utilization, and enhanced responsiveness. Multithreading is particularly useful for handling computationally intensive tasks, I/O operations, and concurrent programming scenarios. However, it also introduces challenges such as thread synchronization and resource sharing, which need to be carefully managed. Overall, multithreading empowers developers to leverage the full potential of modern hardware and optimize program execution by executing tasks concurrently.

JavaScript is a single-threaded-language this means a javascript program starts running right from the top of the code base and finishes at the end of the code base line by line. Each core inside the processor is capable of running the javascript file that is running line by line. So wouldn't it be great if your code is distributed and if you have 8 core processor then your code is running different parts of the code at the same time on different cores of your processor?

Now the world moving into the use of cloud-based architecture the cloud service has enabled us with processors having huge processing power. This makes it essential to learn writing multi-threaded programs in JavaScript.

Writing a multithreaded JavaScript program

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

// Create a new worker instance
const worker = new Worker('./worker.js');
const currentTime = new Date();

// Receive messages from the worker
worker.on('message', function (result) {
  console.log('Result:', result);
  const currentTime = new Date();
  const hours = currentTime.getHours();
  const minutes = currentTime.getMinutes();
  const seconds = currentTime.getSeconds();
  const milliseconds = currentTime.getMilliseconds();

  console.log(`Task Ended: Current time: ${hours}:${minutes}:${seconds}.${milliseconds}`);
});

// Start the computation by sending a message to the worker
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
const seconds = currentTime.getSeconds();
const milliseconds = currentTime.getMilliseconds();

console.log(`Task Started: Current time: ${hours}:${minutes}:${seconds}.${milliseconds}`);
worker.postMessage(inputNumber);

function computeSum(n) {
  let sum = 0;
  for (let i = 1; i <= n; i++) {
    sum += i;
  }
  return sum;
}
computeSum(inputNumber);

The code in main.js does the following:

  1. It imports the Worker class from the worker_threads module.

  2. It sets inputNumber to a large value of 10000000000 to compute the sum of numbers from 1 to that number.

  3. It creates a new instance of the Worker class, passing the path to the worker.js file as an argument. This creates a new worker thread that will execute the code in worker.js.

  4. It captures the current time using the Date object and logs the task start time to the console.

  5. It registers an event listener on the message event of the worker thread. When the worker thread completes its computation and sends a message back, the event listener is triggered, and the received message (the computed result) is logged to the console.

  6. It posts a message to the worker thread, sending the inputNumber as the computation task.

  7. The computeSum function is defined outside the event listener, and it performs the computation of the sum of numbers from 1 to the given input.

Finally, the computeSum function is invoked directly in the main thread as well.

// worker.js file
const { parentPort } = require('worker_threads');

// Receive messages from the main thread
parentPort.on('message', function (inputNumber) {
  console.log('Received input number:', inputNumber);

  // Perform the computation
  const result = computeSum(inputNumber);
  console.log('Computed result:', result);

  // Send the result back to the main thread
  parentPort.postMessage(result);
});

// Function to compute the sum of numbers from 1 to n
function computeSum(n) {
  let sum = 0;
  for (let i = 1; i <= n; i++) {
    sum += i;
  }
  return sum;
}

The code in worker.js does the following:

  1. It uses the parentPort object provided by the worker_threads module to listen for messages from the main thread. The parentPort object represents the communication channel between the worker thread and the main thread.

  2. It registers an event listener on the message event of the worker thread. When a message is received from the main thread, the event listener function is invoked.

  3. The received input number is logged to the console.

  4. The computeSum function is called to perform the computation of the sum of numbers from 1 to the input number.

  5. The computed result is logged to the console.

The result is sent back to the main thread by calling the postMessage method on the parentPort object.

This code was written for a duel core processor where it took 20 seconds to run whereas when run in a single-threaded environment it took 40 seconds.

Results after using this on a dual-core intel processor

When the workers were employed to do particular sub-task of the complete file processing, it cut down the run time from 37 seconds to an impressive 18 seconds. This enables us to leverage multi-threading to distribute the code along worker threads to run on different cores of the CPU. This can be most beneficial when working with cloud CPU architecture of 8 core or above.