Node’s Multithreading Vs Async Performance

Varad
5 min readJul 12, 2023

--

Multitasking with Node.Js

This article focuses on 2 main points

  • Async code and Multi threaded code examples in Node.Js
  • Comparison of their performance on CPU intensive tasks.

Every use case or requirement when writing a piece of code must follow a particular paradigm. A user must strive to achieve best concurrency possible based on either through multitasking or parallelism.

When working with Node.Js, it is always mentioned that JavaScript has a single thread with non blocking execution. This infers that our code will execute without blocking for any IO dependent operation. This way Node.Js achieves best concurrency by leveraging the asynchronous nature of its code interpretation.

But at times you don’t need to use IO dependent operations, and in such cases the non blocking style of executions fails to provide a true sense of concurrency. Such scenarios need multitasking with threads i.e. multithreading approach. An example where this can prove highly beneficial are operations/functions which perform CPU intensive tasks like image processing, data compression/decompression, mathematical calculations, sorting, searching etc.

Let’s see 2 examples where we read numbers from a text file and sort them. We will use the numbers from a number.txt file which has around 27K numbers.

numbers.txt

Asynchronous example

The code below will sort the numbers in ascending order using selection sort. To make it more CPU intensive we will repeat the sorting operation 5 times in a for-loop to showcase the effort of a single-thread’s non-blocking performance on a repetitive task.

const { readFileAsync } = require("../../fs/readFileExample");
const { selectionSort } = require("../sorting/selection");
function readFilesInIterations() {
const startTime = Date.now(); //use this at end to calculate total time.
console.log("Operation started...");
const promises = [];
const iterations = 5;
//Use for loop to perform the activity repeatedly.
for (let i = 0; i < iterations; i++) {
const promise = new Promise((resolve, reject) => {
//Read the file asynchronously
readFileAsync("numbers.txt")
.then(numbers => {
let numbersArray = numbers.split(",");
console.log("Total numbers input:", numbersArray.length);
//perform sorting
numbersArray = selectionSort(numbersArray);
resolve(numbersArray);
})
.catch(err => reject(err));
});
//store promise of each read & sort operation occuring inside the loop
promises.push(promise);
}
Promise.all(promises)
.then(/* (data) => console.log(data) */)
.catch()
.finally(() => {
//Print the total time needed to execute this entire operation.
console.log(`Time spent: ${Math.round((Date.now() - startTime)/1000)} seconds.`);
});
}

When the above code is executed it gives following output:

Operation started...
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
Time spent: 5 seconds.

So the above code is run on a single thread with non blocking execution. Thus the file read operation is done in an asynchronous way, and then the numbers are sorted. Since the execution happens asynchronously, and it is repeated 5 times, and the time taken is 5 seconds, we can assume that each read & sort operation inside the loop took 1 second. The entire code took totally 5 seconds to execute.

Multithread example

Let’s do the same operation with Worker Threads of Node.Js.

Now in this example some primary changes are, reading the file synchronously and using 5 threads instead of 5 iterations of a for-loop to perform it as a repetitive task.

const { Worker, isMainThread, BroadcastChannel } = require("node:worker_threads");
const { readFileSync } = require("../../fs/readFileExample");
const { selectionSort } = require("../sorting/selection");

/* broadcast channel will enable the main thread
to receive messages from worker threads. */
const bc = new BroadcastChannel("test");

/* isMainThread is false
when worker thread is created to execute this file/module. */
if(isMainThread) {
let totalThreads = 5;
let threadCount = 0; //increment this value when a thread finishes execution.
const startTime = Date.now();
console.log("Operation started...")
for (let index = 0; index < totalThreads; index++) {
//Create a worker thread.
//__filename will tell the worker thread to execute the current file.
const worker = new Worker(__filename);
bc.onmessage = (event) => {
/* onmessage() event is invoked on main thread,
when message received from worker thread. */
threadCount++;
if(threadCount === totalThreads) {
/* threadCount === totalThreads is true
when all threads have finished execution. */
console.log(`All threads completed in ${Math.round((Date.now() - startTime)/1000)} seconds.`);
bc.close(); //close the broadcast channel
}
};
}
} else {
//inside a worker thread

const numbers = readFileSync("numbers.txt");
let numbersArray = numbers.split(",");
console.log("Total numbers input:", numbersArray.length);
numbersArray = selectionSort(numbersArray);

bc.postMessage("done"); //send message to main thread.
bc.close(); //close the channel of each thread.
process.exit(1); //terminate the thread.
}

Output:

Operation started...
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
Total numbers input: 27378
All threads completed in 1 seconds.

Here all the 5 threads completed their execution in 1 second.

You can see that with multithreading we are able to achieve a level of decent concurrency in performing CPU intensive tasks. Compared to non blocking execution, multithreading proved to be a better way to get better performance.

Note: I have purposefully used selection sort because of its nested iterations going all the way till the last elements which makes it less efficient on large arrays. This way I can have my CPU to work more.

Note: The readFileAsync() and readFileSync() are my internal functions which actually use fs.readFile() and fs.readFileSync().

Then should you use multithreading for all scenarios in Node.js?

Absolutely not. When you need to perform IO intensive operations that don’t require high CPU utilisation then the non blocking execution proves to be far more efficient and concurrent in Node.js compared to using threads. This is because JavaScript inherently works in an asynchronous manner.

So even if you go for threads, you are bound to write a code, suppose a HTTP request which will be asynchronous, and consequently making the need for threads absolutely unnecessary. Hence as mentioned in the opening paragraph of this article, your choice should depend on the need and nature of operation.

--

--

No responses yet