Know your tool: Event Loop in libuv
Yudel Pan. Watchmaker. 1924
“A computer is a state machine. Streaming programming is necessary for those who do not know how to program finite state machines ”
Alan Cox, approx. Wikipedia
“Know your instrument” - everyone repeats and still trust. They trust the module, trust the framework, trust the example of others.
A favorite question at Node.js interviews is the Event Loop device . And with all that, the obvious fact that this knowledge will be useful to the application developer, few people try to plunge themselves into the device of the event cycle. Basically, everyone is happy with the picture above. Although this is similar to a retelling of a film that you have not watched, but about which a friend told you.
The most difficult thing, probably for me, is to admit my mistakes and agree that I don’t know something. They don’t like to talk and write about mistakes. Basically, everyone loves to write and talk about their successes and good stories, a person tries to build the image of an invincible hero.
But as a rule, mistakes are made precisely out of ignorance, precisely because of superficial judgments, due to the fact that someone spent less time than necessary to study the question posed. The evidence. I know.
Below, I will try to describe my understanding of the event cycle using the example of libuv source code (in tandem with V8, this is the basis of Node.js) , as well as I will join a cohort of people who say: “You need to know your tool”.
By the way, the latter, in modern realities, is becoming a difficult task. Only npm counts, at the moment, almost half a million modules, I'm not talking about the army of repositories on github . But this is how it works to stay in place, you need to run to move, you need 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 plunge into the source code yourself, draw some conclusions, and then return to this text.
Also described below is a huge approximation of what actually happens under the hood of Node.js. Among many others, the note is based precisely on the source code of libuv. I will considerlibrary code base in unix part . The code for win will be different.
Well, at first a little fundamental terminology:
Event-driven 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 the development of the GUI, however, it was also used on the server side. In 1999, serving the Simtel public FTP server, which was popular at that time, its administrator, Den Kegel, noticed that a node on a gigabit channel should be able to handle a load of 10,000 connections in terms of hardware, but the software did not allow this. The problem was associated with a large number of program threads , each of which was created on a separate connection.
The idea of an event loop working in a single thread solved this problem. Similar implementations exist 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 bright representative of such an implementation is the Nginx proxy server .
At the heart of the SOP is the Event Loop , a software design that manages events and messages in a program.
The cycle, in turn, works with asynchronous input / output , or non-blocking input / output, which is a form of data processing 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. A similar technique allows us to conveniently work with asynchronous input / output.
“Hello World!”
Now, let's start with the official “Hello World!” Example of the site 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 (this is by the way the mode that used in Node.js).
Then the cycle is closed (all event observers, signal observers are stopped , the memory allocated for the observers is freed) and the memory reserved by the cycle is freed. We will be interested in the device function-start loop ( uv_run), we’ll see its source code (it’s not entirely original, I deleted lines that are not related to the default mode, therefore the “ mode ” variable is not involved anywhere):
The body of the launch function, as we see, does not start with the “ while ” loop , but with a call to uv__loop_alive . 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 function will simply update the execution time of the event loop and immediately end.
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 action in the loop iteration will also be updating the runtime ( uv__update_time ).
The next step in the iteration is to start the timers.
The structure of the event loop contains the so-called bunch of timers. The timer start function pulls the timer handler from the heap with the smallest time and compares this value with the execution time of the event loop. If the timer is shorter, then this timer stops (it is deleted from the heap, its handler is also deleted from the heap). Next is a check to see if you need to restart it.
In Node.js (JavaScript), we have setInterval and setTimeout functions, in terms of libuv, this is one and the same - a timer ( uv_timer_t ) , with the only difference being that the repeat flag is set on the interval timer ( repeat = 1 ).
An interesting observation: in the case of the retry flag set, the uv_timer_stop function will work twice for the timer handler.
Let's move on to the next step in the iteration of the event loop, namely, the function of launching pending callbacks . Calls are queued . 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, since, you remember,in unix everything is a file .
Next in the iteration, there are two mystical lines:
In fact, these are also callback-start functions, but they have nothing to do with I / O. In fact, these are some kind of internal preparatory actions that it would be nice to complete before starting to perform 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. Preprocessors are also registered in the same way.
Node.js (JavaScript) has no equivalent to these handlers, i.e. we cannot register some kind of callback that would be performed at exactly one of these steps. However, we need to make one caveat using process.nextTick , we can inadvertently execute the code at one of these steps, since this function works directly at the current stage of the event cycle, and this, including, 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, diagram with stackoverflow:
The next interesting step in the iteration is the external I / O operations ( poll (2) ).
Here I combined two steps: calculating the time to perform an external operation and, directly, an external operation.
The calculation of the execution time of an external I / O operation by implementation is similar to the timer start function, since the value of this time is calculated based on the nearest timer. This, incidentally, is achieved non-blocking model ( non-blocking poll ).
The source code of the uv__io_poll function is quite complex and not small. There is multi-threaded work, event observers, callbacks are recorded and work is done with file descriptors .
I will not give the code of this function here, the picture fully reflects the essence of this operation:
The next operation in the command queue of the iteration of the event loop 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 by the same principle and call after external operations. However, in this case, we have the opportunity 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 launch closing handlers.
This function bypasses the linked list.closing handlers and trying to complete the closing for each. If the handler has a special callback to close, then, at the end, this callback is launched.
And the last step of the iteration, this is the familiar uv__loop_alive function . If this function returns a non-zero result, 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
useful links
LibUV: Design Overview
Nodejs.org: The Node.js Event Loop, Timers, and process.nextTick ()
Translating Nodejs.org documentation on
Philip Roberts: What the heck is the event loop anyway?
RisingStack.com: Understanding Node.js Event Loop
Nodesource.com: Understanding Node.js Event Loop
Mozilla.org: EventLoop
Nodejs.org: The Node.js Event Loop, Timers, and process.nextTick ()
Translating Nodejs.org documentation on
Philip Roberts: What the heck is the event loop anyway?
RisingStack.com: Understanding Node.js Event Loop
Nodesource.com: Understanding Node.js Event Loop
Mozilla.org: EventLoop