Nodejs Event Loop

Node.js Event-Loop Mechanism

First off, I should mention that if you really want asynchronous operation A to be processed in a specific order with relation to asynchronous operation B, you should probably write your code such that it guarantees that without relying on the details of exactly what gets to run first. But, that said, I have run into issues where one type of asynchronous operation can "hog" the event loop and starve other types of events and it can be useful to understand what's really going on inside if/when that happens.

Broken down to its core, your question is really about why Immediate2 logs before Timer2 when scheduled from within an I/O callback, but not when called from top level code? Thus it is inconsistent.

This has to do with where the event loop is in its cycle through various checks it is doing when the setTimeout() and setImmediate() are called (when they are scheduled). It is somewhat explained here: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout.

If you look at this somewhat simplified diagram of the event loop (from the above article):

Sample Image

You can see that there are a number of different parts to the event loop cycle. setTimeout() is served by the "timers" block at the top of the diagram. setImmediate() is served in the "check" block near the bottom of the diagram. File I/O is served in the "poll" block in the middle.

So, if you schedule both a setImmediate(fn1) and a setTimeout(fn2, 0) from within a file I/O callback (which is your case for Intermediate2 and Timer2), then the event loop processing happens to be in the poll phase when these two are scheduled. So, the next phase of the event loop is the "check" phase and the setImmediate(fn1) gets processed. Then, after the "check" phase and the "close callbacks" phase, then it cycles back around to the "timers" phase and you get the setTimeout(fn2,0).

If, on the other hand, you call those same two setImmediate() and setTimeout() from code that runs from a different phase of the event loop, then the timer might get processed first before the setImmediate() - it will depend upon exactly where that code was executed from in the event loop cycle.

This structure of the event loop is why some people describe setImmediate() as "it runs right after I/O" because it's positioned in the loop to be processed right after the "poll" phase. If you are in the middle of processing some file I/O in an I/O callback and you want something to run as soon as the stack unwinds, you can use setImmediate() to accomplish that. It will always run after the current I/O callback finishes, but before timers.

Note: Missing from this simplified description is promises which have their own special treatment. Promises are considered microtasks and they have their own queue. They get to run a lot more often. Starting with node v11, they get to run in every phase of the event loop. So, if you have three pending timers that are ready to run and you get to the timer phase of the event loop and call the callback for the first pending timer and in that timer callback, you resolve a promise, then as soon as that timer callback returns back to the system, then it will serve that resolved promise. So, microtasks (such as promises and process.nextTick()) get served (if waiting to run) between every operation in the event loop, not just between phases of the event loop, but even between pending events in the same phase. You can read more about these specifics and the changes in node v11 here: New Changes to the Timers and Microtasks in Node v11.0.0 and above.

I believe this was done to improve the performance of promise-related code as promises became more of a central part of the nodejs architecture for asynchronous operations and there is also some standards-related work in this area too to make this consistent across different JS envrionments.

Here's another reference that covers part of this:

Nodejs Event Loop - interaction with top-level code

Node.js event loop's phase speciality

What is meaning of "each phase is special in its own way"?

Each phase has its own job to do and some work pretty differently than others. For example:

The timers phase checks the head of a sorted linked list of pending timers and calls any callbacks for timers in that list whose time has arrived.

The poll phase calls the OS to get any pending operations such as incoming network activity.

The pending callbacks phase is a FIFO queue of certain types of system events that don't fit into other phases.

Each of these phases has a similar high-level function (retrieve and process a specific type of events who are ready to run), but all work completely differently inside (thus are special in its own way).

In which phase of Nodejs event loop resolved promises' callbacks get executed?

There's an important semantic with microtasks and nextTicks which depends on the Node version.

Before Node v11, nextTick queue was executed between each phase of the event loop (timers, I/O, immediates, close handlers are the four phases). Therefore, before Node v11, promise callbacks were also executed between each phase in the event loop. (I've written about this in detail here: https://blog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa)

However, starting from Node v11, event loop jumps to microtask queue whenever a microtask is added to the nextTick queue as part of the execution of the program. You can experiment this with the following snippet. The same applies to nextTick queue too. You can read more here: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3

setImmediate(() => console.log('timeout1'));
setImmediate(() => {
console.log('timeout2')
Promise.resolve().then(() => console.log('promise'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));

The output of the above code changes depending on the Node.js version as follows:

$ node -v
v10.19.0
$ node test.js
timeout1
timeout2
timeout3
timeout4
next tick

$ nvm use 11
Now using node v11.15.0 (npm v6.7.0)
$ node test.js
timeout1
timeout2
next tick
timeout3
timeout4

Therefore it's important to know that: nextTicks and microtasks have an even higher priority in Node versions >=11, because they get the chance to be processed within the current phase of the event loop. But in earlier Node versions, nextTicks and microtasks are executed at the end of each phase of the loop.

On a side note, it's important to know that microtasks queue is a part of v8 engine and not maintained in Node.js runtime. However, Node.js event loop instructs v8 to run all microtasks, once Node.js finishes with the nextTick queue. Therefore, promise callbacks are executed after nextTick queue.



Related Topics



Leave a reply



Submit