Telerik blogs
JavaScriptT2 Light_1200x303

An in-depth understanding of the event loop may be one of the things many developers neglect. This post covers a detailed explanation of what it is and how it works.

Once upon a time, we only had a single process. If we look into the MS-DOS and the first Apple OS, they could run one thing at a time. There was no background task or running of multiple programs. When running a program, if you were to switch to another program, it automatically stopped the current one running, which was quite limiting. In order to solve this problem, cooperative multitasking was introduced, which makes things much more efficient.

The way that cooperative multitasking works is you have an application that is running, and then it gets to a stage where it takes a break so that other things can run. This is primarily via a function written into the application called yield, which signals back to the OS that the application has completed in order for other applications to continue execution. Still, the issue here is that it depends on the user’s application calling the yield function, i.e., if the yield function is not called, the programs keep running.

Cooperative multitasking was an improvement—we could run multiple things, but we still had some problems with it, and stability was the primary problem.

That led to the introduction of preemptive multitasking, where there is no reliance on the application anymore—instead, the OS can pause any application at a given time. It will pause the application, save the state somewhere else and load another application. It helps the operating system work without depending on the applications, thereby gaining more stability and efficiency.

So many people kept researching how to improve performance, especially when there were multi-core processors released in the early 2000s. Questions like “How can multiple-core processors be utilized for maximum performance?” led to a lot of research that brought about symmetric multi-threading (SMT).

With SMT the OS can take advantage of new assembly-level instructions in the processor itself. Hence, we execute instructions in stages.

Introduction to Thread and Processes

The process can be defined as the program under execution, while a thread is a basic execution unit. Each program may have several processes in it, and each process has several threads executing in it.

A thread is a basic unit of execution or CPU utilization; it comprises a thread ID, a program counter, a register set and a stack. It also shares with other threads belonging to the same process its code section, data section and other operating system resources, such as open files and signals.

A traditional/heavyweight process has a single thread of control. If a processor has multiple threads of control, it can perform more than one task at a time.

Overview of processes and threads: single thread process has one thread aimed at all boxes: code, data, files, registers, stacks. Multi thread process has three different threads aimed at stacks of boxes: code, registers, register; data, registers, registers; files, registers, registers
Overview of processes and threads

This post will cover the event loop in Node.js, how it works, its function, async and sync programming, and how to use the event loop effectively.

Prerequisites

To follow along with this post, you need to have:

  • Basic knowledge of Node.js
  • Node installed on your PC
  • A code editor

What Is the Event Loop?

JavaScript is an asynchronous single-threaded language—it is one call stack and it can only do one thing at a time. This call stack is present in the JavaScript engine and all the code of the JavaScript is executed in the call stack.

function b() {
  console.log("b");
}

b();
console.log("end");

Whenever any JavaScript program is executed, a global execution context is created; it is then moved to the call stack. Looking at the code above, in order for JavaScript to run the code, it first creates the global execution context in the call stack, then the function “b” is allocated memory and moved to the call stack for execution, which then prints “b” to the console.

Once the function is executed, it’s moved out of the call stack, and the following code begins execution. That is how the JavaScript engine runs its program.

console.log("Start");

setTimeout(function cb() {
  console.log("Callback");
}, 5000);

console.log("End");

In the scenario of having a timeout or delay executing code, the global execution context is first created and pushed inside the call stack. The whole code block will run line by line, starting by printing Start to the console, which is executed inside the global execution context. It moves to the setTimeout function, which registers a callback and also starts a timer of 5000 ms and moves to the following line without waiting; this line prints End to the console, then the global execution context pops off the call stack since we are done executing the code. While all these things are happening, the timer is still running.

As soon as the timer is done counting, the callback function needs to be executed, which means it has to find a way to move the callback inside the call stack to execute the function; this is where the event loop and callback queue come into play. Because the callback function cannot go directly into the call stack, it has to pass through the callback queue. After the timer expires, the callback function is moved into the callback queue, and the event loop helps put the callback functions from the callback queue into the call stack.

The event loop serves as a gatekeeper that checks for a pending function in the callback queue. If yes, it is pushed inside the call stack.

Let’s go through one more code example before we continue.

console.log("start");

setTimeout(function cb() {
  console.log("Callback");
}, 5000);

fetch("https://api.netflix.com")
.then(function cbNet() {
  console.log("let have fun with Netflix")
});

console.log("End")

So in the scenario above, we have a setTimeout function, waiting for the time before it executes the function, and then the fetch function, waiting for data to be fetched from the given endpoint.

Like the previous examples, it starts the code execution by executing the first line of code in the call stack, which will print Start to the console. It moves it out of the call stack and moves to the timeout function, stored as a callback until the timeout expires. It immediately moves to the following line, stored as a callback until the data is fetched from the endpoint. Lastly, it moves to the last line of the code, which prints End to the console; once the timeout expires or the data is received from the endpoint, the callback is moved to the callback queue, which is then moved sequentially by the event loop to the call stack for execution.

Understanding Sync and Async Programming

Synchronous programming is a type of programming where code is executed from the top of the file to the bottom of the file without skipping any line.

Asynchronous programming, on the other hand, round-executes the code from top to bottom but will run into certain asynchronous functions or code which will need to split up and be separately executed because it needs to wait or do some operations before the function can be executed—which often takes some time.

let a = 1;
let b = 2;

console.log(a);
console.log(b);

In the code above, the output will be one coming first and two coming second. This is a synchronous code that runs without delay.

let a = 1;
let b = 2;
console.log(a);

setTimeout(function cs(){
  console.log("asyncronous");
}, 100)

console.log(b);

In the code above, the timeout function needs a delay of 100 ms, leading to the code execution waiting until the timeout expires. The output will be 1, 2, “asynchronous” because the timeout will be delayed, giving the other function time to execute.

The Event Loop in Node.js

Node.js is single-threaded, and the event loop runs on a single thread called the main thread, but there is a bit of difference with Node.js. There are certain things in C++ found in Node.js, having a ratio of 2/3 JavaScript: 1/3 C++.

Let’s look at examples:

// Synchronous version
const crypto = require('crypto');

const Num = 3;

for (let i = 0; i < Num; i++) {
  crypto.pbkdf2Sync("secrect_key", "water", 1000, 512, 'sha512');
}
// Asynchronous version
const crypto = require('crypto');

const Num = 3;

for (let i = 0; i < Num; i++) {
  crypto.pbkdf2("secrect_key", "water", 1000, 512, 'sha512', ()=>{});
}

Looking into the crypto module, let’s check out the pbkd (Password-Based Key Derivation) function. This can run in both sync and async; running this code synchronously with the number of requests set to 3 will make the code run three times waiting for one to complete before running the other.

That takes more time to complete the execution, while running the code in async will run the three in parallel, reducing the execution time.

Node does not spin up a new thread for each request. Instead, it uses a pre-allocated number of threads called the thread pool; the default is four which will be reused for all the work, so once the request starts, it will be assigning them to the threads on the thread pool. Hence, once all the threads in the thread pool are busy, it puts the request in a queue until one of the working threads is free for assigning.

Event Loop Phases

Here’s the list of phases in the event loop.

Pool Phase

All synchronous JavaScript code will be executed in the pool phase of the event loop.

console.log("1")
console.log("2")

The code above is synchronous and will be executed in the pool phase since there is no delay to the code.

Timer

These functions execute their tasks as callbacks based on a specific timeout, i.e., the timeout must expire before the function is executed.

console.log("start");

setTimeout(function cb() {
  console.log("Callback");
}, 5000);

console.log("End");

Input/Output Callbacks

The process whereby an application does not need to wait for a request to get specific data before it executes; the code is executed asynchronously.

fs.readFile("/nsa.md", (error, data) => {
  if (error) throw error;
});

mainFunc();

Using the fs.readFile as a classic Input/Output operation, a request to read filesystem is passed to the OS by Node, and then the code will continue to execute the following line of code: mainFunc (). Once the request is completed, a callback function will be placed in the callback queue.

Idle

When the event loop works on the internal functions of any callbacks.

SetImmediate

A special timer, introduced by Node.js, that executes a callback immediately after an event loop phase becomes idle. Once setImmediate is called, it is executed before other timer functions.

Close Event

This event loop state will consistently execute all the closing events of a function, e.g., process.exit().

Conclusion

This post discussed processes, threads, asynchronous and synchronous functions, and the event loop and how it works in Node.js.


Chinedu
About the Author

Chinedu Imoh

Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Related Posts

Comments

Comments are disabled in preview mode.