How the Event Loop Works in Node.js

Written by roman9131 | Published 2022/11/22
Tech Story Tags: event-loop | nodejs-developer | java-event-loop | nodejs | nodejs-tutorial | nodejs-apps | node.js-event-loop | learning-to-code

TLDRNode.js is a single-threaded event-driven platform that is capable of running non-blocking, asynchronous programming. The event loop mechanism is also implemented in the LibUV library. The library is written in Javascript alone and in c++ and Javascript to operate correctly. It is also written in [C](https://www.hackernoon.com/images/tgb3usu.jpeg) for [unix/core.c#L384) shown below (but you can also look for [win)(https://github.com) for the [win] library shown below.via the TL;DR App

Node.js is a single-threaded event-driven platform that is capable of running non-blocking, asynchronous programming. These functionalities of Node.js make it memory efficient. The Event Loop allows Node.js to perform non-blocking I/O operations despite the fact that JavaScript is single-threaded. It is done by assigning operations to the operating system whenever and wherever possible.

In this article, I will try to explain the mechanism of concurrency at Node.js which is the Event Loop.

The first thing to consider is that Node.js is written in Javascript alone and in c++ and Javascript to operate correctly. There are many libraries upon which node depends, but the two foremost dependencies that handle most node.js operations V8 and LIBUV.

These two dependencies allow us to write pure JavaScript code that runs in Node.js while still giving us access to the file reading functionality implemented in LIBUV.

The event loop mechanism is also implemented in this library.

Let’s open the source code of this library and see how the mechanism is implemented. Source code of LIBUV written in C for unix shown below (but you can also look for win):

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);  // Check loop alive ----- (1)
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);   // Check timer phase ----- (2)

    can_sleep =
        QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles);

    uv__run_pending(loop);  // Check pending callbacks ----- (3)
    uv__run_idle(loop);     // Check idle  ------ (4)
    uv__run_prepare(loop);  // Check prepare ----- (5)

    timeout = 0;
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

    uv__metrics_inc_loop_count(loop);

    uv__io_poll(loop, timeout);  // Check poll ----- (6)

    for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++)
      uv__run_pending(loop);

    uv__metrics_update_idle_time(loop);

    uv__run_check(loop);            // Check run checks ------ (7)
    uv__run_closing_handles(loop);  // Check close callbacks ------ (8)

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  return r;
}

Let’s make a scheme of work on the diagram. The following diagram shows a simplified overview of the event loop’s order of operations.

Consists of two parts Microtasks and Macrotasks which are divided into phases (Each box will be referred to as a “phase” of the event loop).

Phases Overview

  • Timers: this phase executes callbacks scheduled by setTimeout() and setInterval().

  • Pending callbacks: executes I/O callbacks deferred to the next loop iteration.

  • Idle, Prepare: only used internally.

  • Poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.

  • Check: setImmediate() callbacks are invoked here.

  • Close callbacks: some close callbacks, e.g. socket.on('close', ...).

Each phase has a FIFO queue of callbacks to execute. When the event loop enters a given phase, it will perform any operations specific to that phase and then execute callbacks on that phase’s queue until either the queue is exhausted, or until the maximum number of callbacks is fulfilled. When the queue is exhausted or the callback limit is reached, the event loop will move to the next phase.

Also this library is responsible for providing Nodejs with multithreading or the ability to provide a pool of threads in a Nodejs process for synchronous tasks to ride on. The thread pool consists of four threads(can configure it up to 128), created to handle heavy-duty tasks that shouldn’t be on the main thread. And with this set-up, our application is not blocked by these tasks.

some APIs — — as listed below, use the thread pool created by libuv:

  • dns.lookup()
  • All zlib APIs that are synchronous
  • All fs APIs that are synchronous except fs.FSWatcher()
  • Asynchronous crypto APIs

The above list can be further categorised into CPU-intensive operations and I/O-intensive operations.

Let’s take a look at the following code and how it will be processed

const fs = require('fs');
const http = require('http');

const requestListener = function (req, res) {
  res.writeHead(200);
  res.end('Hello, World!');
}

const server = http.createServer(requestListener);
server.listen(3000);

console.log("Start"); // ----- (A)

setTimeout(() => console.log("setTimeout -- 1"), 0); // ----- (B)

setImmediate(() => console.log("setImmediate")); // ----- (C)

fs.readFile(__filename, () => { // ----- (D)
    console.log("readFile callback"); // ----- (E)
    setTimeout(
      () => console.log("setTimeout in readFile"),
       0
    ); // ----- (F)
    setImmediate(
      () => console.log("setImmediate in readFile")
    ); // ----- (G)
    process.nextTick(
      () => console.log("nextTick in readFile")
    ); // ----- (H)
});

Promise.resolve().then(() => { // ----- (I)
  console.log("Promise success callback"); // ----- (J)
  process.nextTick(
    () => console.log("Promise nextTick")
  ); // ----- (K)
  setImmediate(
    () => console.log("Promise setImmediate")
  ); // ----- (L)
});

process.nextTick(() => console.log("nextTick")); // ----- (M)

setTimeout(() => console.log("setTimeout -- 2"), 0); // ----- (N)

console.log('End'); // ----- (O)

So when Node.js is reading file, it executes synchronously consoles (A, O).the rest of the asynchronous operations will be divided into micro and macro tasks, after the execution of the synchronous code, our Event Loop starts working.

The order of execution of asynchronous operations presented below:

Conclusion

The first thing to note is that your code in Nodejs is single-threaded. And this does not mean that Node is running on a single thread. The question ‘is Node single-threaded?’ is always confusing because Node runs on V8 and Libuv.

By itself, V8 works one thread but with the help the Libuv library is using the event Loop, distribute operations into different queues (phases), and for some heavy operations(it is first sent to the thread pool for execution by different threads, then the raised ones return to the phase), after which the Event Loop, according to the underlying mechanism, starts sending callbacks for execution to the main thread.

That's the whole concurrency mechanism in Node.js, which is called the Event Loop.

Hope it was useful for you!

Thanks for reading!


Written by roman9131 | I am a Senior Software Engineer with 6 years of experience.
Published by HackerNoon on 2022/11/22