Yes, you read it right. Multiple threads in a NodeJs app.
But isn't NodeJs a single-threaded? I know this is hitting your mind.
Well, that is the old story about nodeJs. It's a bit old now, and it's spreading across the web. It would be a shame if it doesn't support multi-threads as on the other programming languages.
That's why NodeJs introduced worker-threads. It's provided out of the box in the NodeJs core modules.
Now let me be clear about this thing, that the way multi-threading is implemented in NodeJs is not the same as we would implement in other languages like Java, Python, etc.
Let us understand things, how it used to work in the older NodeJs :
Whenever a request is sent to a NodeJs application, this single-threaded application accepts the request (it's non-blocking IO right, so it has to accept it).
It starts processing the request all of a sudden, as per the code mentioned inside the controller representing the route, the request came for.
At the same time, there can be more than one request hitting this same application, and even those requests get accepted and begin to get processed.
All these are done by a single-threaded event loop.
And this is how event loop processes a request, let's take an example of a router code of a node's expressJs application.
Let's say two requests to send email comes at the same time. Both the request starts processing.
By the code, the above functions get executed for each request.
Question is, will both the requests be run in parallel?
No, not. NodeJs is single-threaded and it will run each thing at a time.
Then does it mean that each request will run one after another in sequential order? Did I not say that both the requests get accepted and start processing as soon as it gets accepted?
Yes, I did say that. Both of the above statements are correct and things happen concurrently but not parallelly (You may google about the difference between these two to understand it more).
Let's go through the controller function for the request, to understand how this happens in each line.
Extracts the three variables message, from, to from the request body. This is a synchronous operation.
Create a template for account confirmation and stores it to a variable called template . This is a synchronous operation.
Generates an email object and stores it to a variable called email. This is a synchronous operation.
Pushes the email object to the email service. This is an asynchronous operation and takes a bit of time to happen. And whenever the operation is completed, then the response is sent back to the client.
Now that we have gone through the code, let's see it executing for both requests together.
NodeJs event-loop is single-threaded, and it will execute the controller function for the initial request first and following gets executed :
Line 1 gets executed and let's say it takes 4ms for example.
Line 2 gets executed and this takes 3ms.
Line 3 gets executed and this takes 3 ms.
Now, this task to push email message
to email service gets executed, and let's assume that it takes 14ms. (These are usually IO calls like HTTP or some messaging call to a message queue, hence it's asynchronous)
Let's deviate a bit from this flow and understand the internal thread pool in nodeJs.
Now, something we need to understand about nodeJs is there is an internal thread pool maintained in nodeJs and these threads in the internal thread pool are used for specific asynchronous tasks such as HTTP calls, Database operation, libraries like bcrypt
use this for encryption, File operations, etc
Hmm, So nodeJS do use multiple threads?
Yes, they do, but it's used internally by nodeJs itself not putting the burden on the developer to deal with the heavy task of managing threads and to bring asynchronously in the code.
Now, this doesn't mean the internal thread pool has unlimited threads. It just has 4 by default. But you can easily change it according to your system resources by setting an environment variable in two ways :
While running the app :
UV_THREADPOOL_SIZE=64 node index.js
- Inside the app, at the beginning of the main file.
process.env.UV_THREADPOOL_SIZE=64
Whenever an asynchronous task comes occurs, its either transferred to the thread pool or queued until a thread is free. And once the task completes the callback associated with that async task is invoked.
Now that we have enough information about the internal thread pool, let's move back to the case we were discussing.
In the 4th step in the process we discussed, it takes 14 ms, and here comes the part where nodeJs become different from the rest of the languages.
When the 4th step is transferred to a thread in the internal thread pool, it need not wait for the task to complete. The main event loop becomes free and hence it starts processing the next request.
So we see that before even a request completes, another request begins to process. And in the middle of the second request being processed, the first requests 4th step completes and the callback gets invoked before the second request finishes. And once the callback finishes executing, the second request continues to process.
This is how NodeJs deals with concurrent requests using just one single-threaded event loop.
This is how the old nodeJs used to work. So what's the new way NodeJS works?
Hmm, the answer is it works the same way it used to. But something new has come which allows the developer to create new threads in their application apart from the already available internal thread pool.
And this possible using worker-threads module, which is part of nodeJs from version 12+.
Unlike other languages, here in nodeJs these threads can be used and reused by every request or task. That means we create a thread with which we mention what it must do, by passing a js file to it.
Now the developer can pass data to the worker and it gives the output.
Please have a look at the diagram to relate to what I explained.
Now you can see an example, on how to use a worker thread.
// index.js
const { Worker } = require('worker_threads');
const workerScriptFilePath = require.resolve('./worker-script.js');
const worker = new Worker(workerScriptFilePath);
worker.on('message', (output) => console.log(message));
worker.on('error', (error) => console.log(error));
worker.on('exit', (code) => {
if (code !== 0)
throw new Error(`Worker stopped with exit code ${code}`);
});
/**
Once we have added all the event listeners to the worker, we send message data to the worker, to be processed.
**/
worker.postMessage('this is a lower case sentence');
The above code is of the main file: index.js
// worker-script.js
const { parentPort } = require('worker_threads');
parentPort.once('message', (message) => {
const output = capitalise(message);
parentPort.postMessage(output);
});
function capitalise(text) {
return text.toUpperCase();
}
The above code is of the worker file: worker-script.js
I am sure you must have understood from the above codes, on how you could pass some data to a worker and get the output.
And till the time the worker processes the message, the main application can do whatever task requires to be done. And once the worker finishes the task, the main application can receive the message from the worker and do the next steps.
There is a good library for using worker-threads in nodeJs with ease, called V-blaze. I will write an article on multiple applications of worker-threads in nodeJs using V-blaze.
V-blaze comes with a worker thread pool out of the box, and also there is a very interesting feature called nanoJo for nodeJs in V-blaze. I will update the link to the article here soon.
By this, we come to an end to this article. I believe the beginners would have got a good understanding of worker threads and also how the concurrent requests are handled in nodeJs.
As a whole how nodeJs is not a single-threaded language anymore.
Hope you liked it. If yes do give some claps for me. If not, please do send me some suggestions. And you can also suggest to me on topics which I can write.
You can connect with me via the following: Twitter, Instagram
Top comments (0)