Event loop?
First of all, wtf is an event loop? #
If you are working with Javascript you have heard about Event Loop. The event loop is implemented in multiple languages like Python in asyncio, Php through a library called ReactPhp (and others). Some languages can create new threads but they have to enable extensions (like php), and in some cases (like Python) there is a big limitation caused by the GIL (Global interpreter lock) that avoids executing code in multiple threads at once.
In simple terms (the way we like), an event loop is a loop that is listening for events to dispatch callbacks, this technique or pattern allows code to be executed in a pseudo-parallel way. Why pseudo-parallel? because code is not being executed simultaneously, it looks like is parallel-executed but is paused and executed during the process, we will see this more in-depth.
Each implementation of the event loop differs, for example, the web event loop is not the same as the node.js event loop but they are similar. In this post, we are going to focus on the node.js event loop.
How an event loop works? #
Let’s start with the phases it goes through:
I have to admit the names suck, it’s so difficult to imagine what a timers phase does, but we will see in a moment what are those.
Things to have in mind to understand better this explanation:
- These phases are just for synchronous code
- Each phase has a FIFO (first in, first out) queue
- The event loop is going through each phase all the time
Timers #
This phase executes the code scheduled by setTimeout
and setInterval
.
Whatever you put as a callback for these two functions is executed as soon as it
reaches the threshold and it is not blocked waiting for another process in
another phrase, for example the poll phase (we will see it in a moment).
Pending callbacks #
Responsible for executing some callbacks from the OS like TCP errors, nothing to
go deep here, we just need to know that if we have an ECONNREFUSED
while
trying to connect, the error will be queued in this phase.
Poll #
This is where callbacks related to I/O are processed, for example file system, and http and it is a bit tricky because it has conditions related to other other phases, let’s check:
- If the queue is not empty it will be dequeued until the queue is empty or the system hard limit has been reached. So, what is the hard limit? for example in Linux, if you get 1024 events for 48 iterations it stops, as it’s said in a reference from node.js repo.
- If the poll queue is empty, if there are callbacks in the queue of the check phase, the poll phase ends, and the loop moves to the Check phase to execute the callbacks immediately, otherwise, the poll phase will wait for more callbacks were added to the queue. When the queue is empty, it checks if there are callbacks in the Timers queue to take the closest threshold as a limit for the execution (this is because we want to jump as soon as possible to execute Timers callbacks) and if there are callbacks ready the event loop will move to the Timers phase to execute them.
Check #
This phase schedule the setTimmediate callbacks and they get executed as soon the poll queue is empty.
Close callbacks #
As the name suggests, callbacks that are related to closing
processes, for
example the one you get when calling socket.destroy()
, these kind of callbacks
are queued in this phase.
Let’s check an example (taken from node.js website)
|
|
As we can see here, the Timers phase schedule the callback for setTimeout
in line 10, then someAsyncOperation is called in line 17 and the callback is
executed BUT blocks for an extra 10ms. Reading the file (95ms) + the callback
block (10ms) give us 105 ms, after that time, Timers phase can to execute
the callback, so the setTimeout
function took 105 ms instead of 100ms.
Sometimes is better to see the code running the event loop to have a better picture of it, well… here you have, this is a piece of code of the library libuv which is the master piece behind the event loop.
|
|
And as an extra, this is a good visualization, just for understanding the logic, it could not be precise but it helps us to have a picture in our mind of how it works.
A few things to have in mind:
- process.nextTick takes precedence over everything (executed as soon as possible)
- Promise await and callbacks take precedence over setImmediate
- setImmediate takes precedence over setTimeout and setInterval in the poll phase
- setTimeout and setInterval takes precedence over I/O operations
Links #
- What is the event loop?
- Github issue about poll and timers phases
- What the heck is the event loop anyway? (for web)
Conclusions #
Having a better understanding of the execution model helps us to take a better decisions in terms of how we want to run our code choosing the appropriate method to set the priority + understand possible blocks of other functions caused by a long-running task (like the example).