The worker thread module in Node.js allows us to run JavaScript code in parallel, preventing CPU-intensive operations from blocking the main thread of our application. Before delving deeper into this module, it's important to understand a few key concepts. Familiarity with these concepts will help us use and benefit from worker threads effectively.
CPU Core
A CPU core serves as the fundamental unit responsible for executing instructions within a computer system. When exploring computing systems, it is essential to distinguish between single-core and multi-core systems. In a multi-core system, there are multiple CPU cores available, each capable of independent execution. Worker threads in Node.js can be distributed across multiple CPU cores to achieve parallel execution and leverage the full processing power of the system.
In contrast, a single-core system has only one CPU core available for executing instructions. When worker threads are introduced in such a system, they run alongside the main thread but do not achieve true parallelism. Instead, a technique called time-sharing is used. This means that when a worker thread is scheduled to run, it occupies the sole CPU core, causing the main thread and other threads to wait until the core becomes available again. As a result, the main thread gets blocked, leading to decreased application performance.
Threads
Threads play a vital role in dividing the execution of a program into concurrent tasks, enabling both concurrency and parallelism. Essentially, threads serve as the smallest units of execution within a program. However, Node.js, which is primarily single-threaded, introduces the concept of threads through the underlying libuv library. This powerful library handles asynchronous I/O operations and manages the event loop efficiently. It achieves this by utilizing a dynamic thread pool that adapts based on system conditions and configurations.
On the other hand, the V8 engine, responsible for executing JavaScript code in Node.js, employs multiple threads internally for tasks like garbage collection and other optimizations. These threads are managed by the V8 engine itself and are not directly exposed or configurable within Node.js. Overall, the combined use of threads, the libuv library, and the V8 engine enables Node.js to handle concurrent and parallel tasks effectively, enhancing performance and responsiveness.
Worker Thread
We have covered the fundamental concepts . Now, we can proceed to learning how to utilize worker threads in our applications.
To initialize a new Node.js application, run the following command to generate a package.json file with default settings:
npm init -y
By default, Node.js comes bundled with the worker thread module. To create a simple server, let's install Express
npm i express
In our package.json we can go ahead and create a start script
//package.json
"scripts": {
"start": "node index.js"
}
Your updated package.json file should look like this 👇👇
Now let's create a simple program that simulates blocking the main thread. This program will loop one million times and write its output to a file.
//index.js
const fs = require('node:fs');
const express = require('express');
const app = express();
app.get('/write', async (req, res) => {
try {
for(let i = 0; i < 1000000 ; i++){
fs.appendFileSync( 'test.txt',` ${i} `);
}
return res.status(200).json("Done");
} catch (error) {
return res.status(500).json("Error Occured ");
}
});
app.get('/' , (req , res)=> {
try {
return res.status(200).json("Health check route");
} catch (error) {
return res.status(500).json("Error Occured ");
}
})
app.listen(3001 , ()=> {
console.log(" App is running in 3001 ");
});
The code inside the /write route will block the main thread when executed. This means it will consume all available resources, preventing other incoming requests from being processed.
To fix this issue and avoid blocking the main thread , we can utilize the worker thread .
Let's modify our code to use the worker thread . Let's create a separate file called 'worker.js'
//worker.js
const fs = require('fs');
const { parentPort } = require('worker_threads');
const {
workerData
} = require("worker_threads");
const count = workerData
for(let i = 0; i < count ; i++){
fs.appendFileSync( 'test.txt',` ${i} `);
}
parentPort.postMessage(`Done`)
In the worker.js file, we import the required modules fs, parentPort, and workerData from the worker_threads module.
The workerData variable allows passing data from the main thread to the worker thread.
Inside the worker thread, we perform the expensive operation, which in this case is appending a sequence of numbers to the test.txt file.
After the operation is completed, we use parentPort.postMessage() to send a message back to the main thread, indicating that the task is done.
We can now proceed to modify our index.js
//index.js
const express = require('express');
const app = express();
const {Worker} = require('worker_threads');
app.get('/write', async (req, res) => {
try {
const worker = new Worker('./worker.js' , {workerData : 1000000});
worker.on("message", msg => {
return res.status(200).json(msg);
});
worker.on("error", err => console.error(err));
} catch (error) {
console.log(error);
return res.status(500).json("Error Occured ");
}
});
app.get('/' , (req , res)=> {
try {
return res.status(200).json("Health check route");
} catch (error) {
return res.status(500).json("Error Occured ");
}
})
app.listen(3001 , ()=> {
console.log(" App is running in 3001 ");
});
In the updated index.js file , When a GET request is made to the
/write
route a new worker thread is created using the Worker class from theworker_threads
moduleThe
workerData
option is used to pass the value 1000000 to the worker thread.Two event listeners, message and error, are added to the worker thread. The message listener handles messages sent from the worker thread, while the error listener handles any errors that occur.
With this implementation, the /write route will spawn a worker thread to execute the blocking loop. The main thread remains responsive to handle other incoming requests, improving the overall performance and responsiveness of the application.
Top comments (5)
Wonderful, well written article Williams, I'll definitely save this
Thanks
Please check Threadosaurus, it simplifies using worker threads greatly: npmjs.com/package/threadosaurus?ac...
Does It uses virtual core/thread in cpu?
The worker thread module can use both physical and virtual core depending on the underlying CPU architecture.