Yudel Peng. Watchmaker. 1924“A computer is a state machine. Stream programming is necessary for those who cannot program finite automata. ”
Alan Cox, approx. Wikipedia
“Know your instrument” - they say everything around and still trust. They trust the module, trust the framework, trust someone else's example.
')
A favorite question at
Node.js interviews is the
Event Loop device. And for all that, the obvious fact that this knowledge will be useful to the application developer, few people try to immerse themselves in the device of the event cycle. Basically, everyone is satisfied with the picture above. Although it looks like a retelling of a film that you did not watch, but a friend told you about.

The most difficult, probably for me, is to admit my mistakes and agree that I don’t know something. Error do not like to talk and write. Basically, everyone loves to write and talk about their successes and good stories, a person tries to build an image of an invincible hero.
But as a rule, mistakes are made precisely because of ignorance, precisely because of superficial judgments, due to the fact that someone spent less time than necessary to study the question posed. Evidence. I know.
Below, I will try to describe my understanding of the event loop using the example of the
libuv source code
(in tandem with V8, this is the basis of Node.js) , and I will also join a cohort of people who say: “You need to know your tool”.
By the way, the latter, in modern realities, becomes a difficult task. Only one
npm has, at the current moment, almost half a million modules, I’m not even talking about the army of repositories on
github . But everything is arranged in such a way that in order to stay in place, you need to run, to move, you have to run twice as fast.
This note is primarily a reminder to me, a reminder to be more attentive. To the reader, I recommend to immerse yourself in the source code, draw some conclusions, and then return to this text.
Also, described below is a huge approximation of what actually happens under the hood. Node.js. Among many others, the note is based precisely on the libuv source code. I will look
at the library code base in the unix part . The code for win will be different.
Well, in the beginning, some fundamental terminology:
Event-oriented programming (SOP, Event-Driven Programming / EDP) is a programming paradigm in which program execution is determined by events.
The SOP paradigm is actively used in GUI development, however, it has also been applied on the server side. In 1999, servicing Simtel's public FTP server, which was popular at the time, his administrator,
Dan Kegel, noticed that a node on a gigabit channel would have to handle 10,000 connections in hardware, but the software did not allow this.
The problem was associated with a large number of software threads , each of which was created on a separate connection.
The idea of a single-event event loop solved this problem. There are similar implementations not only in the JavaScript world (Node.js). For example,
Asyncio and
Twisted in Python,
EventMachine and
Celluloid in Ruby,
Vert.x in Java. Another prominent representative of this implementation is the
Nginx proxy server.
At the heart of the SOP is the
Event Loop (Event Loop) - a software construct that deals with the scheduling of events and messages in the program.
The loop, in turn, works with
asynchronous I / O , or
non-blocking I / O , which is a data processing form that allows other processes to continue execution before the transfer is completed.
Callback function - the ability to transfer executable code as one of the parameters of another code. This technique allows us to conveniently work with asynchronous I / O.
“Hello World!”
Now let's start with the official example “Hello World!” Of
http://docs.libuv.org :

The example is simple, the necessary RAM is reserved and the
structure of the event loop is initialized, then it starts in
the default mode (by the way, this is the mode used in Node.js).
Then there is a closing of the cycle (stopping of all observers of events, observers of
signals , release of memory allocated for observers) and release of memory reserved by the cycle itself. We will be interested in the function of the start-up loop (
uv_run ), let's see its source code (it is not quite original, I deleted strings not related to the default mode, so in the example the variable “
mode ” does not participate anywhere):

The body of the start function, as we see, does not begin with the “
while ”
loop , but with the
uv__loop_alive call. In turn, this function checks for the presence of active
handlers or
requests :

The result of the execution of this function will determine whether the “
while ” cycle starts or not. In the absence of requests or handlers, the start-up function will simply update the execution time of the event loop and immediately terminate.
If there is something to process (
r! = 0 ) and the stop flag is not set (
stop_flag == 0 ), then the cycle will start. And the first
step in the loop iteration will also be the update of the runtime (
uv__update_time ).


The next step in the iteration is to start the timers.

The structure of the event loop contains a so-called
bunch of timers. The timer start function pulls from the heap the timer handler with the shortest time and compares this value with the execution time of the event loop. If the timer is shorter, this timer stops (removed from the heap, its handler is also deleted from the heap). Next comes a check whether you need to restart it.
In Node.js (JavaScript), we have the
setInterval and
setTimeout functions, in terms of libuv, this is the same thing - the
timer ( uv_timer_t ) , with the only difference that the interval timer has a repeat flag (
repeat = 1 ).
An interesting observation: in the case of the repeat flag set, the uv_timer_stop function will work twice for the timer handler.Let us proceed to the next step in the iteration of the event loop, namely, the function of starting
pending callbacks . Calls are stored in the
queue . These can be handlers for reading or writing a file,
TCP or
UDP connections, in general, any
I / O operations , because the type does not really matter, because, as you remember,
in unix everything is a file .


Next in the iteration are two mystical lines:

In fact, these are also callback start-up functions, but they have nothing to do with I / O. In fact, these are some internal preparatory actions that it would be nice to take before starting the execution of external operations (meaning I / O). In the case of “Hello World”, there are no such handlers, but there are examples on the site where such callbacks are registered.

In this example, the idle handler does nothing, it will be executed until the counter reaches a certain value. In the same way, preparatory processors are registered (prepare).
Node.js (JavaScript) has no equivalent for these handlers, i.e. we cannot register any callback which would be executed on one of these steps. However, one reservation must be made using
process.nextTick , we can unintentionally execute the code in one of these steps, since this function is triggered directly at the current stage of the event cycle, and this, in particular, may be
uv__run_idle or
uv__run_prepare . The
process.nextTick function itself has nothing to do with the libuv library.
On this topic (the work of
process.nextTick ) I still have an old, but still relevant, stackoverflow diagram:

The next stage of the iteration is the most interesting - these are external I / O operations (
poll (2) ).
Here I combined two steps: calculating the time for performing an external operation and, directly, an external operation.

The calculation of the execution time of the external I / O operation is similar to the implementation of the timer start function, since the value of this time is calculated based on the nearest timer. This, by the way, and achieved a non-blocking model (
non-blocking poll ).


The source code of the
uv__io_poll function
is quite complex and not small. There is a multi-threaded work, event observers, callbacks are registered and work is being done with
file descriptors .
I will not give here the code of this function, the picture completely reflects the essence of this operation:


The next operation in the command queue of the event loop iteration is
uv__run_check . It is essentially identical to the functions
uv__run_idle and
uv__run_prepare , i.e. This is the launch of callbacks that register on the same principle and call after external operations. However, in this case, we have the ability to register such handlers from Node.js. This is the
setImmediate function (i.e., immediate execution after an external I / O operation).
The penultimate step is to start the closing handlers.
This function bypasses the
linked list of closing handlers and tries to complete the closure for each. If the handler has a special callback for closing, then, at the end, this callback is started.


And the last step of the iteration, this is already a familiar function
uv__loop_alive . If this function returns a nonzero result, then the event loop will start a new iteration.
***
If you have any comments or additions, I will be glad to see them in the comments or write to
artur.basak.devingrodno@gmail.com