DEV Community

Cover image for Node.js: event-loop with practical examples. A big picture (part 6/7)
boolfalse
boolfalse

Posted on

Node.js: event-loop with practical examples. A big picture (part 6/7)

NGNTJS - part 6


In the previous part 5 of NGNTJS article series we had some understanding about thread pool with a practical real life example.
In this part, we will look at different examples (step by step) and through these examples we will try to understand how event-loop works in practice.

[Image from unsplash.com]

Before preparing this series of articles, and especially this part, a lot of material was studied, in which the following logical sequence can be observed: first, what is an event-loop, and then how it works.
Of course, there is no transition here either (previous: part 5, but as an author, I think it's worth starting to explore event-loop with practical examples, to understand how it works, and then get a bigger picture of what it actually is.

Example 1․ microtask and timer queues

setTimeout(() => console.log("setTimeout 1"), 0);
setTimeout(() => {
  console.log("setTimeout 2");
  process.nextTick(() => console.log("setTimeout 2 nextTick"))
}, 0);
setTimeout(() => console.log("setTimeout 3"), 0);

process.nextTick(() => console.log("nextTick 1"));
process.nextTick(() => {
  console.log("nextTick 2");
  process.nextTick(() => console.log("nextTick 2 nextTick"));
});
process.nextTick(() => console.log("nextTick 3"));

Promise.resolve().then(() => console.log("Promise 1"));
Promise.resolve().then(() => {
  console.log("Promise 2");
  process.nextTick(() => console.log("Promise 2 nextTick"));
});
Promise.resolve().then(() => console.log("Promise 3"));
Enter fullscreen mode Exit fullscreen mode

The result will be:

nextTick 1
nextTick 2
nextTick 3
nextTick 2 nextTick
Promise 1
Promise 2
Promise 3
Promise 2 nextTick
setTimeout 1
setTimeout 2
setTimeout 3
setTimeout 2 nextTick
Enter fullscreen mode Exit fullscreen mode

Example 2․ I/O queue and timer queue anomaly

const fs = require("fs");

setTimeout(() => console.log("setTimeout"), 0);
fs.readFile(__filename, () => console.log("readFile"));
// for (let i = 0; i < 1000000000; i++) {}
Enter fullscreen mode Exit fullscreen mode

The result will be something like this:

setTimeout
readFile
// anomaly
readFile
setTimeout
Enter fullscreen mode Exit fullscreen mode

Example 3․ I/O queue and I/O polling

const fs = require("fs");

fs.readFile(__filename, () => {
  console.log("readFile");
});
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("Promise"));
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));

for (let i = 0; i < 1000000000; i++) {}
Enter fullscreen mode Exit fullscreen mode

The result will be:

nextTick
Promise
setTimeout
setImmediate
readFile
Enter fullscreen mode Exit fullscreen mode

Example 4․ check queue & timer queue anomaly

setTimeout(() => console.log("setTimeout"));
setImmediate(() => console.log("setImmediate"));
Enter fullscreen mode Exit fullscreen mode

The result will be something like this:

setTimeout
setImmediate
// anomaly
setImmediate
setTimeout
Enter fullscreen mode Exit fullscreen mode

Example 5․ Close queue in the end

const fs = require("fs");

const readableStream = fs.createReadStream(__filename);
readableStream.on("close", () => {
  console.log("readableStream");
});
readableStream.close();

setImmediate(() => console.log("setImmediate"));
setTimeout(() => console.log("setTimeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
process.nextTick(() => console.log("nextTick"));
Enter fullscreen mode Exit fullscreen mode

The result will be:

nextTick
Promise
setTimeout
setImmediate
readableStream
Enter fullscreen mode Exit fullscreen mode

You can also find examples on GitHub Gist.

In these examples, the microtask (nextTick and Promise), timer, I/O (I/O polling), check and close queues are sequentially passed.

Now, since here we already understand what the event-loop mechanism is, and we know the stages of work, we can look at the Node.js architecture in a "big" picture.
Using the keyword "Node.js architecture" on the internet, you can find many visuals, which are made by different authors according to certain requirements, to represent this or that component or the nature of the work they want.
Below I will post the visual made by me, which presents the work in Node.js Runtime in a high-level. It more oriented towards a logical chain than individual program blocks.

[Interaction between libUV's event-loop and other components in the Node.js runtime]

Let's briefly describe the meaning of the new terms mentioned in the visual, that not yet explained in the articles before:

  • event-demultiplexer - responsible for monitoring various I/O sources and receiving events. If the I/O source requires a blocking operation, event-DEMUX will pass the given task to the C/C++ field, where the given task will be executed. Once the task is finished, the C/C++ side will notify the event-dispatcher, which in turn will send it to the event-loop to be added to a specific queue.
  • event-dispatcher - is responsible for event management, sending and receiving events
  • kqueue - BSD-based mechanism for event notifications (macOS, FreeBSD, NetBSD)
  • IOCP (input/output completion ports) - BSD-based mechanism for the event notifications (macOS, FreeBSD, NetBSD)
  • event ports - Solaris-specific mechanism for the event notifications (Oracle Solaris, OpenSolaris)
  • epoll - A Linux-specific mechanism for the event notifications

In addition, libUV provides other useful features in apart from above described features:

  • Asynchronous I/O - libUV provides a common interface for asynchronous I/O operations across different OSs.
  • Signal handling - libUV provides an API: Signal handlers are registered to handle OS signals such as SIGINT or SIGTERM.
  • File system operations - libUV provides an API to perform file system operations such as file writing or file reading.
  • Network I/O - libUV provides an API to perform network I/O operations, such as creating and managing TCP/UDP sockets.
  • Child processes management - libUV provides an API for creating and managing child_processes.
  • ...

Coming to this point, we can understand the idea of Reactor Pattern.

[Image from the mini-series Chernobyl]

In this part of the series of NGNTJS articles we studied the event-loop mechanism through practical examples, got an understanding about the Reactor Pattern.
Next, in part 7 we will discuss JavaScript language, engine, runtime and environment. Then we will get to know modern JavaScript runtime/environments, such as Deno and Bun, and much more.


If you liked this article, feel free to follow me here. 😇

To explore projects working with various modern technologies, you can follow me on GitHub, where I actively publicize much of my work.

For more information, you can visit my website: https://boolfalse.com/

Top comments (2)

Collapse
 
manchicken profile image
Mike Stemle

I love a good event loop breakdown. Thanks!

Collapse
 
boolfalse profile image
boolfalse

I appreciate your feedback. Feel free to recommend writing something interesting as an article for later.