Skip to main content

Node.js: Event Loop

Event Loop

 

Coming from the world of PHP, Node.js was a frustrating experience, especially when it came to event loops, callbacks, promises, and so on. It took me a few hours to understand the fundamentals of the Node.js event loop. So, for everyone else out there (who doesn't understand), I'll try to explain it as simply as I can.

As the official docs say

What is the Event Loop?

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed.


When Node.js starts, it initializes a single thread known as the event loop. Node.js employs the event loop to handle asynchronous operations within applications.

An example of how an event loop works 

1. Check to see if any of the timer functions listed below have been completed -

  • SetTimeout
  • SetInterval
  • SetImmediate

2. Examine whether any outstanding OS tasks have been completed. As an example, consider an HTTP server that is listening on a specific port.

3. Check to see if long-running operations have been completed.

All of the above operations are tracked internally by Node.js. For the sake of simplicity, let us assume it keeps three different arrays for tracking operations such as pendingTimeouts, pendingOSTasks, and pendingCallbacks. (Note: These variables are named at random for clarity.) Node.js adds the operation and its callback to the relevant array for each operation performed.

For example, when a GET request is made to an external resource, Node.js adds it to the pendingOSTasks array. When a response is received, the associated callback is executed, and the event is removed from the array.

Node.js checks between event loop runs to see if it is waiting for any operations or timers and shuts down if there aren't any. In the preceding example, the decision is made by determining whether each array element is empty.


Actual steps/phases that occur within each Event loop

1. Timers: Callbacks scheduled by setTimeout() and setInterval() are executed during this phase. A timer specifies the threshold after which a provided callback may be executed rather than the exact time the callback should be executed. In theory, the poll phase determines when timers are executed.

2. Pending Callbacks: The majority of the callbacks will be processed here. The asynchronous I/O request is recorded in the queue, and the main call stack can resume normal operations. The I/O callbacks of completed or errored out I/O operations are processed in this phase of the Event Loop.

3. Idle: During this phase, the Event Loop executes any callbacks' internal operations. Technically, there is no direct influence on this phase or its duration. There is no mechanism in place to ensure code execution during this phase. It is primarily used for information gathering and planning of what needs to be done during the next tick of the event loop. To illustrate this phase, consider how a chef might begin preparing the ingredients and equipment needed to prepare the dessert course while the main course is still in the oven. As a customer, you don't need to be aware of what's going on as long as your food is served in the correct order and on time.

4. Polling: This is the phase in which all JavaScript code is executed, beginning at the top of the file and working down. Depending on the code, it may execute immediately or queue something to be executed during a future tick of the Event Loop. During this phase, the Event Loop manages the I/O workload by calling functions in the queue until the queue is empty and calculating how long it should wait before proceeding to the next phase. All callbacks in this phase are synchronously called in the order in which they were added to the queue, from oldest to newest. This is the phase in which our application may be stalled if any of these callbacks are slow or are not executed asynchronously. Please keep in mind that this phase is entirely optional. Depending on the state of your application, it may not occur on every tick. For example, if any setImmediate() timers are scheduled, Node.js will skip this phase during the current tick and move to the setImmediate() phase.

5. SetImmediate Callbacks: Node.js has a special timer, setImmediate(), and its callbacks are executed during this phase. This phase runs as soon as the poll phase becomes idle.  If setImmediate() is scheduled during the I/O cycle, it will always be executed before any other timers, regardless of the number of timers present.

6. Close events: This phase handles all close event callbacks. This is the point at which the Event Loop has completed one cycle and is ready to move on to the next. It is primarily used to clean the application's state.

Each phase has its own FIFO queue of callbacks to execute. When the event loop enters each phase, it performs operations related to that phase, then executes callbacks in the phase's queue until all the callbacks are exhausted or the maximum number of callbacks are executed.

Blocking the event loop

Node.js sends time-consuming operations to the C++ API and its threads, such as I/O callbacks. This simulates "multithreading" within a single-threaded Node.js process, allowing the main runtime to continue executing our code without stopping. This provides Node.js with the advantages of an asynchronous non-blocking I/O interface.

Despite the fact that Node.js uses non-blocking I/O, several core modules have expensive API operations. Encryption, compression, and file system are examples of these modules. Node.js passes those asynchronous API methods to the Worker Pool because they are not intended to be executed within the Event Loop.

The Event Loop is responsible for keeping the application running. When running a server, for example, the Event Loop is responsible for detecting new client requests and responding to each one. This means that it processes all client requests and responses. As a result, if the Event Loop is blocked on a response for any client at any time, current and upcoming clients will not receive a response until the blocked request has been processed.

Consider the simple code example below, which will cause the event loop to become blocked. In this case, the code will take ten seconds to respond to a single request and more than ten seconds to process all concurrent and pending requests.

Conclusions

  • To avoid the complexity of writing multithreaded code, Node.js processes are single-threaded. The Event Loop, on the other hand, allows I/O tasks to be offloaded to C++ APIs. This allows the Node.js core runtime to continue running JavaScript code while the C++ APIs handle asynchronous I/O operations in the background. This assists in the development of non-blocking code.
  • Timers, I/O callbacks, idle phase, polling, setImmediate callbacks execution, and close events callbacks are the six main phases of the Event Loop.
  • When a phase is completed, the application advances to the next tick, and all phases are repeated, beginning with timers, until there is nothing left to process.


Comments

Popular posts from this blog

Node.js: Thread pool

Let us begin by defining a thread. Wikipedia states that - In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. The implementation of threads and processes differs between operating systems, but in most cases a thread is a component of a process.   I n Node.js, there are two types of threads: one Event Loop, also known as the main thread, and a pool of k Workers in a Worker Pool, also known as a thread pool. The libuv library maintains a pool of threads that Node.js uses in the background to perform long-running operations without blocking its main thread. To handle "expensive" tasks, Node.js employs the Worker Pool. This includes I/O for which an operating system does not provide a non-blocking version, as well as CPU-intensive tasks in particular. So, in essence, the threads are executed by the processor. Let's say a system or m...

Understanding SAML Metadata

Single sign-on  ( SSO )  is an authentication mechanism that allows a user to log in to numerous linked but separate software systems using a single set of credentials. We have two basic entities when it comes to Single Sign-On (SSO) - Identity Provider (IdP) - This entity is in charge of verifying the user's identity and communicating user information with the Service Provider (SP). In a nutshell, the identity provider delivers identification data.   Service Provider (SP) - This entity is responsible for providing services to the user. From the IdP, it obtains the user's identity. Consider the following scenario to better understand SSO: You've lately started working at XYZ, a new company. You've been given a work email address as well as access to a dashboard. After logging in, you'll see icons for all of the company's external services, including Salesforce, Jira, and others. When you click on the Salesforce icon, a background procedure occurs, and before y...