NodeJS Event Loop

Node JS event loop

The libuv event loop (or any other API involving the loop or handles, for that matter) is not thread-safe except where stated otherwise.

There is only one thread that executes JavaScript code and this is the thread where the event loop is running. The execution of callbacks (know that every userland code in a running Node.js application is a callback) is done by the event loop. We will cover that in depth a bit later.

Libuv by default creates a thread pool with four threads to offload asynchronous work to. Whenever possible, libuv will use those asynchronous interfaces, avoiding usage of the thread pool. The default size of the pool can be overridden by setting the environment variable UV_THREADPOOL_SIZE.

Low resource cost per connection.

Node uses a small number of threads to handle many clients. In Node there are two types of threads: one Event Loop (aka the main loop, main thread, event thread, etc.), and a pool of k Workers in a Worker Pool (aka the threadpool).

  • The Event Loop executes the JavaScript callbacks registered for events, and is also responsible for fulfilling non-blocking asynchronous requests like network I/O.

  • Node's Worker Pool is implemented in libuv, which exposes a general task submission API. Node uses the Worker Pool to handle "expensive" tasks. This includes I/O for which an operating system does not provide a non-blocking version, as well as particularly CPU-intensive tasks.

The Event Loop and the Worker Pool maintain queues for pending events and pending tasks, respectively. In truth, the Event Loop does not actually maintain a queue. Instead, it has a collection of file descriptors that it asks the operating system to monitor. When the operating system says that one of these file descriptors is ready, the Event Loop translates it to the appropriate event and invokes the callback(s) associated with that event. In contrast, the Worker Pool uses a real queue whose entries are tasks to be processed. A Worker pops a task from this queue and works on it, and when finished the Worker raises an "At least one task is finished" event for the Event Loop.

Don't block Event Loop rule

If a thread is taking a long time to execute a callback (Event Loop) or a task (Worker), we call it "blocked". While a thread is blocked working on behalf of one client, it cannot handle requests from any other clients.

You should make sure you never block the Event Loop. In other words, each of your JavaScript callbacks should complete quickly. This of course also applies to your await's, your Promise.then's, and so on.

Because Node handles many clients with few threads, if a thread blocks handling one client's request, then pending client requests may not get a turn until the thread finishes its callback or task. The fair treatment of clients is thus the responsibility of your application. This means that you shouldn't do too much work for any client in any single callback or task.

This is part of why Node can scale well, but it also means that you are responsible for ensuring fair scheduling.

A good way to ensure this is to reason about the "computational complexity" of your callbacks. If your callback takes a constant number of steps no matter what its arguments are, then you'll always give every pending client a fair turn. If your callback takes a different number of steps depending on its arguments, then you should think about how long the arguments might be.

  • Node uses the Google V8 engine for JavaScript, which is quite fast for many common operations. Exceptions to this rule are regexps and JSON operations.

  • Your goal should be to minimize the variation in Task times, and you should use Task partitioning to accomplish this.

Event Loop Algorithm

When they begin, Node applications first complete an initialisation phase, require'ing modules and registering callbacks for events. Node applications then enter the Event Loop, responding to incoming client requests by executing the appropriate callback. This callback executes synchronously, and may register asynchronous requests to continue processing after it completes. The callbacks for these asynchronous requests will also be executed on the Event Loop.

  • When Node.js starts, it initializes the event loop which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

  • Event loop has several phases:

    • timers - this phase executes callbacks scheduled by setTimeout() and setInterval().

    • I/O Callbacks - executes almost all callbacks with the exception of close callbacks, the ones scheduled by timers, and setImmediate().

    • idle, prepare - only used internally.

    • poll - retrieve new I/O events; node will block here when appropriate.

    • check - setImmediate()

    • close - close callbacks handlers

    • Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

  • 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, then execute callbacks in that phase's queue until the queue has been exhausted or the maximum number of callbacks has executed.

  • When the queue has been exhausted or the callback limit is reached, the event loop will move to the next phase, and so on.

poll

The poll phase has two main functions:

  • Executing scripts for timers whose threshold has elapsed, then

  • Processing events in the poll queue.

check

This phase allows a person to execute callbacks immediately after the poll phase has completed. If the poll phase becomes idle and scripts have been queued with setImmediate(), the event loop may continue to the check phase rather than waiting.

setImmediate() is actually a special timer that runs in a separate phase of the event loop.

Главным преимуществом использования setImmediate() вместо setTimeout() является то, что setImmediate() всегда будет выполняться перед любыми таймерами, если они запланированы в цикле ввода/вывода, независимо от количества присутствующих таймеров.

nextTick()

process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation completes, regardless of the current phase of the event loop.

This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.

In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate(), but this is an artifact of the past which is unlikely to change.

Why use process.nextTick():

  1. Allow users to handle errors, cleanup any then unneeded resources, or perhaps try the request again before the event loop continues.

  2. At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.

  3. (+) Many async I/O operations

  4. (-) All should be async

  5. Avoid block event loop.

Worker Pool

Node.js runs JavaScript code in the Event Loop (initialization and callbacks), and offers a Worker Pool to handle expensive tasks like file I/O. Node is fast when the work associated with each client at any given time is "small".

Its default size is 4, but it can be changed at startup time by setting the UV_THREADPOOL_SIZE environment variable to any value (the absolute maximum is 128).

The threadpool is global and shared across all event loops. When a particular function makes use of the threadpool libuv preallocates and initializes the maximum number of threads allowed by UV_THREADPOOL_SIZE. This causes a relatively minor memory overhead (~1MB for 128 threads) but increases the performance of threading at runtime.

Last updated