NodeJS is an open-source and cross-platform JavaScript runtime environment built on top of Chrome’s V8 JavaScript Engine enabling high performance and scalability. NodeJS uses the process model instead of the traditional web server model, which makes it super useful for web development.
In this tutorial, we briefly introduced the NodeJS process model. Before deep diving into this concept, let’s get an idea about the traditional web server model.
Traditional Web Server Model
Before the advent of NodeJS, the web server model consisted of a thread pool where a dedicated thread handled each user request. In this model each time when a new user request comes in, it is assigned to a different thread, and in any event, when a thread is not available to process the request, it needs to wait for the next available thread. A thread is freed only when the request is processed, a response is returned, and the thread is returned back to the thread pool. This makes the traditional web server model synchronous and blocking.
NodeJS Process Model
Contrary to the traditional web server model, NodeJS uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. The NodeJS process model can be explained with three architectural features of NodeJS.
- Single-threaded event loop
- Non-Blocking I/O Model
- Event-driven and Asynchronous by default
Single Threaded Event Loop
NodeJS runs on a single-threaded environment which means each user request processes on a single thread only. This makes it use lesser resources and run smoothly using events and emitters.
Events are a crucial paradigm of the NodeJS process. Events are actions that instruct the runtime what and when something needs to be completed. Event Emitters are response object instances that can be subscribed to and acted upon to perform operations. Event Emitters emit events based on certain predefined events accepting a callback. According to MDN web docs, event loops are responsible for executing the code, collecting and processing events, and executing queued sub-tasks.
NodeJS has two types of threads: one Event loop also referred to as the main thread, and the k Workers also referred to as the background thread. When a new user request comes in, it is placed in an event queue. Every request consists of a synchronous and asynchronous part. The synchronous part of the request is handled on the main thread while the asynchronous part is handled in the background via the k Workers/ background threads.
Even though we talk about multiple threads, NodeJS is said to be single-threaded as all the requests are received on a single thread, and execution of the asynchronous processes takes place internally.
Non-Blocking I/O Model
Blocking codes or operations are the ones that need to be completed entirely before moving on to another operation. Non-blocking codes are asynchronous and accept callback functions to operate.
As mentioned, every request has a synchronous and asynchronous part. The main thread of NodeJS does not keep waiting for the background thread to complete the asynchronous I/O operations. The main thread keeps switching between other requests to process their synchronous part while the background thread process the asynchronous part.
Event-Driven and Asynchronous By Default
Once the execution of the background thread is complete, the background thread emits events to notify the main thread. Callback functions are associated with asynchronous processes. If the main thread is not free, the request waits for the main thread to be free and then takes up the callback request for further execution.
To provide concurrency, I/O events and callbacks, and other time-consuming operations are asynchronous by default. Node architecture uses libuv, a C library built specifically for NodeJS for handling most asynchronous I/O operations.
Here we will take a look at some examples to understand the above concepts.
Example:
The file reader module provides us with both synchronous and asynchronous methods to read files.
const fs = require("fs");
// Synchronous method
const data = fs.readFileSync("./textFile.txt", "utf-8");
console.log(data);
Output:
Example:
The asynchronous method always uses callbacks and events to resolve its operation.
const fs = require("fs");
// Asynchronous method
const data = fs.readFile("./textFile.txt", "utf-8", (error, data) => {
if (error) {
throw new error;
}
console.log(data);
});
Output:
Example:
You can also use async/await among other methods to handle asynchronous operations.
const fs = require("fs").promises;
// Using async/await method
const readDataFromFile = async () => {
const data = await fs.readFile("./textFile.txt", "utf-8");
console.log(data);
}
readDataFromFile();
Output:
Example:
Listening to events and asynchronous operations.
const fs = require("fs");
const dataStream = fs.createReadStream("./textFile.txt", "utf-8");
// readable event
dataStream.on("readable", () => {
console.log(dataStream.read());
});
// end event
dataStream.on("end", () => console.log("File Reading Completed"));
Output:
Learn more about asynchronous programming here
Summary
NodeJS has brought into existence an event-driven, non-blocking I/O model called the process model. Earlier a traditional model is used which is complex, synchronous and blocking while the process model is asynchronous and non-blocking, above all it is single-threaded and event-driven. Hope this tutorial helps you to learn the concept of the NodeJS process model in depth.
References
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop