Earlier, NodeJS was not favored for CPU-heavy operations due to its architecture, which is further optimized for I/O activities. Normally a server can quickly become puzzled by a CPU-intensive workload. Let’s take an example suppose we have two endpoints one is to perform simple, non-CPU tasks While the other has to handle complex CPU-intensive operations/tasks that take approx 12 seconds to complete. If your server gets occupied by a CPU-intensive request, it won’t be able to promptly respond to the non-CPU-intensive request. The problem generally arises when we exploit the golden rule of NodeJS i.e. Don’t Block the Event Loop. Earlier NodeJS is not designed for CPU-intensive tasks because of its single-threaded architecture. This doesn’t mean that we can’t utilize it for CPU-intensive tasks. Worker threads are here to rescue you.
1. Worker threads: Multitasking in NodeJS
The main work of
Worker threads is to perform complex and heavy javascript tasks. It makes the process easy to run the javascript code in parallel which makes it faster and more efficient. Without even disturbing the main thread we can perform complex and heavy tasks. Worker threads are specifically useful for CPU-intensive tasks as traditionally
NodeJS development services is single-threaded. It is a type of thread that is assigned a particular task or workload, to handle computational tasks or tasks running in the background while the main thread continues to execute its primary duties(handling I/O operations and handling user interface).
1.1 Why Use Worker Threads?
- Efficiency: By using worker threads, time-consuming tasks can be executed in parallel which provides speed and efficiency and allows the main thread to remain responsive
- Concurrency: Making better use of CPU resources by running multiple tasks in parallel. Worker threads run parallel to the main thread which allows the user to perform multiple tasks simultaneously. This method is ideal for the operations like – Data Processing, file compression
- Separate V8 Instances: Each worker thread runs in its own V8 instance, which means it has its own memory. This helps to prevent the main thread from being blocked by long-running operations
- No Shared Memory: To share memory NodeJS provides
SharedArrayBuffer
to share memory between threads. Unlike other environments, it does not share memory directly
1.2 Advantages of Worker Threads
- Concurrency: Enables parallel execution of tasks without blocking the main thread, improving performance
- Non-blocking I/O: Allows efficient handling of multiple I/O operations simultaneously
- Improved Performance: Boosts performance by distributing tasks across multiple threads
- Multithreading in Node.js: Enhances scalability in Node.js by offloading heavy operations to worker threads
1.3 Usecases of Worker Threads
- CPU-Intensive Operations: Useful for data processing, image manipulation, encryption, and other heavy computations
- Background Tasks: Ideal for tasks like sending emails, processing large files, or handling notifications in the background
- Concurrent API Calls: Helps manage multiple API calls concurrently without slowing down the application
- Data Processing: Suitable for working with large datasets or running complex algorithms without impacting responsiveness
1.4 Example
For web environments
Web Workers
are used to implement worker threads. We can use Web workers for performing tasks like computation or network requests without freezing the UI. Here are some steps to implement :
1.4.1 Create a new file named worker.js
//worker.js
self.onmessage = function(event) {
console.log("Message received ", event.data);
//to perform some computations and tasks
let result = 0;
for(let j = 0; j
1.4.2 Create the Main Javascript file to create a worker file and communicate with it
//main.js
//creating a new web worker
const worker = new Worker("worker.js");
//setting up a listener to receive messages from the worker
worker.onmessage = function(event) {
console.log("Response received from the worker thread :", event.data);
};
//Sending message to the worker
worker.postMessage("Start the calculation");
//continue doing other task in the main thread
console.log("Main thread is free to perform other task");
1.4.3 Code Breakdown
The code defines a:
1.4.3.1 Worker File
- Listen to messages coming from the main thread using
self.onmessage
self.postMessage
method is used to send the result of the background task to the main thread
1.4.3.2 Main File
- Create a new worker thread using new Worker("worker.js")
worker.postMessage("Start the computation")
is used to send a message to the worker thread
2. Example in NodeJS
In NodeJS there is a module named
worker_threads
which enables the javascript to run in parallel.
2.1 Example
.
//workerthreads.js
const { isMainThread, workerData, parentPort } = require('worker_threads');
if (!isMainThread) {
//do CPU intensive task
//data sent from the main thread is available in Data
}
//main.js
const { worker, isMainThread } = require('worker_threads');
if (isMainThread) {
const worker1 = new Worker('worker.js', {
Data: dataToPassToNewThread
});
worker.on('message', (data) => {
});
worker.on('error', (err) => {
});
worker.on('exit', (code) => {
});
}
2.1.1 Code Breakdown
- In the above example const
{ worker, isMainThread } = require('worker_threads');
is used to load the worker_thread
module
isMainThread
returns the boolean value which denotes whether we are on the main thread or not
worker
class is used to represent an independent javascript execution thread
new Worker('worker.js')
is used to represent the path to the worker's main script
- Data is used to pass any value to the worker we created
parentport
is a type of communication channel with the main thread
3. When to use worker threads in JavaScript?
- Heavy Computation: When you have a collection of CPU-intensive tasks there is the possibility that it could block the main thread
- Asynchronous Processing: For complex tasks/computations like data processing, image manipulation, etc that can run independently of the main thread
4. Worker Thread Pooling
The collection of active worker threads that can be used to complete an incoming job is called
Worker thread pool
. On the arrival of a new task, it can be assigned to a worker thread who is available at that time. When the worker completes the task the result is shared with the parent, and the worker again becomes idle and ready to accept the new assignments. if
Thread Polling
is implemented properly, it can increase speed by reducing the overhead of spawning new threads.
5. Conclusion
In April 2019 NodeJS v12 was launched in Worker Threads which were enabled by default in this version and were now supported. To reduce execution time this method can be used by NodeJS applications with CPU-intensive workloads. As we know serverless platforms charge based on execution time so it plays a significant role for NodeJS serverless functions. Using several CPU cores can result in enhanced performance and reduced costs.