Introduction to libuv - The Node.js Event Loop

Background - Node.js, libev and libuv

The Node.js project began in 2009 as a JavaScript environment decoupled from the browser. Using Google’s V8 and Marc Lehmann’s libev, Node.js combined a model of I/O - asynchronous event-driven - with a language that was well suited to the style of programming.

As Node.js grew in popularity, it was important to make it work on platforms other than Linux, such as Windows. However, libev ran only on *nix platforms. (The Windows equivalent of kernel event notification mechanisms like kqueue or (e)poll is IOCP.)

libuv was an built as abstraction around libev or IOCP depending on the platform, providing users a cross-platform and high-performance event-driven API based on libev. In the node-v0.9.0 version of libuv, libev was removed.

Contrasting libev vs libuv:

libuv is asynchronous, and libev is synchronous multiplexing IO multiplexing.

libev is a simple encapsulation of system I/O reuse. Basically, it solves the problem of different APIs between epoll and kqueue. (it) ensures that programs written using libev’s API can run on most *nix platforms. However, the disadvantages of libev are also obvious. Because it basically just encapsulates the Event Library, it is inconvenient to use. For example, accept(3) requires manual setnonblocking after connection. EAGAIN, EWOULDBLOCK, and EINTER need to be detected when reading from a socket. This is also the root reason why most people think that asynchronous programs are difficult to write.

Since its release, libuv has continued to mature and become a high quality standalone library for system programming. Users outside of Node.js include Mozilla’s Rust programming language, and a variety of language bindings.

This book and the code is based on libuv version v1.3.0.

What is libuv?

libuv is a cross-platform C library that provides support for an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.

According to its developer, Ben Noordhuis, the name libuv had no specific meaning. Since people kept asking about it, he made it up. libuv = Unicorn Velociraptor. The joke ended up becoming the official logo for the library.

libuv logo

libuv logo: A Unicorn Velociraptor

libvu and Node.js

libvu was developed for use in Node.js as its event loop mechanism.

nodejs Event Loop Architecture

Node.js Event Loop Architecture and libvu (Image credit: https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4)

How to Install libvu?

To install libvu on your machine, please refer to installation instructions here.

Event loops

In event-driven programming, an application expresses interest in certain events and respond to them when they occur. The responsibility of gathering events from the operating system or monitoring other sources of events is handled by libuv, and the user can register callbacks to be invoked when an event occurs. The event-loop usually keeps running forever. In pseudocode:

while there are still events to process:
    e = get the next event
    if there is a callback associated with e:
        call the callback

Some examples of events are:

  • File is ready for writing
  • A socket has data ready to be read
  • A timer has timed out

This event loop is encapsulated by uv_run() – the end-all function when using libuv.

The most common activity of systems programs is to deal with input and output, rather than a lot of number-crunching. The problem with using conventional input/output functions (read, fprintf, etc.) is that they are blocking. The actual write to a hard disk or reading from a network, takes a disproportionately long time compared to the speed of the processor. The functions don’t return until the task is done, so that your program is doing nothing. For programs which require high performance this is a major roadblock as other activities and other I/O operations are kept waiting.

One of the standard solutions is to use threads. Each blocking I/O operation is started in a separate thread (or in a thread pool). When the blocking function gets invoked in the thread, the processor can schedule another thread to run, which actually needs the CPU.

The approach followed by libuv uses another style, which is the asynchronous, non-blocking style. Most modern operating systems provide event notification subsystems. For example, a normal read call on a socket would block until the sender actually sent something. Instead, the application can request the operating system to watch the socket and put an event notification in the queue. The application can inspect the events at its convenience (perhaps doing some number crunching before to use the processor to the maximum) and grab the data. It is asynchronous because the application expressed interest at one point, then used the data at another point (in time and space). It is non-blocking because the application process was free to do other tasks. This fits in well with libuv’s event-loop approach, since the operating system events can be treated as just another libuv event. The non-blocking ensures that other events can continue to be handled as fast as they come in (depending on the capacity of the hardware of course).

Note

How the I/O is run in the background is not of our concern, but due to the way our computer hardware works, with the thread as the basic unit of the processor, libuv and OSes will usually run background/worker threads and/or polling to perform tasks in a non-blocking manner.

Bert Belder, one of the libuv core developers has a small video explaining the architecture of libuv and its background. If you have no prior experience with either libuv or libev, it is a quick, useful watch.

libuv’s event loop is explained in more detail in the documentation.

Hello World Using libuv and Event Loop

With the basics out of the way, lets write our first libuv program. It does nothing, except start a loop which will exit immediately.

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    uv_loop_init(loop);

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);
    free(loop);
    return 0;
}

This program quits immediately because it has no events to process. A libuv event loop has to be told to watch out for events using the various API functions.

Starting with libuv v1.0, users should allocate the memory for the loops before initializing it with uv_loop_init(uv_loop_t *). This allows you to plug in custom memory management. Remember to de-initialize the loop using uv_loop_close(uv_loop_t *) and then delete the storage. The examples never close loops since the program quits after the loop ends and the system will reclaim memory. Production grade projects, especially long running systems programs, should take care to release correctly.

libuv Default loop

A default loop is provided by libuv and can be accessed using uv_default_loop(). You should use this loop if you only want a single loop.

Note

Node.js uses the default loop as its main loop. If you are writing bindings you should be aware of this.

Error handling in libuv

Initialization functions or synchronous functions which may fail return a negative number on error. Async functions that may fail will pass a status parameter to their callbacks. The error messages are defined as UV_E* constants.

You can use the uv_strerror(int) and uv_err_name(int) functions to get a const char * describing the error or the error name respectively.

I/O read callbacks (such as for files and sockets) are passed a parameter nread. If nread is less than 0, there was an error (UV_EOF is the end of file error, which you may want to handle differently).

libuv - Handles and Requests

libuv works by the user expressing interest in particular events. This is usually done by creating a handle to an I/O device, timer or process. Handles are opaque structs named as uv_TYPE_t where type signifies what the handle is used for.

libuv watchers

Handles represent long-lived objects. Async operations on such handles are identified using requests. A request is short-lived (usually used across only one callback) and usually indicates one I/O operation on a handle. Requests are used to preserve context between the initiation and the callback of individual actions. For example, an UDP socket is represented by a uv_udp_t, while individual writes to the socket use a uv_udp_send_t structure that is passed to the callback after the write is done.

Handles are setup by a corresponding:

uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)

function.

Callbacks are functions which are called by libuv whenever an event the watcher is interested in has taken place. Application specific logic will usually be implemented in the callback. For example, an IO watcher’s callback will receive the data read from a file, a timer callback will be triggered on timeout and so on.

Idling

Here is an example of using an idle handle. The callback is called once on every turn of the event loop. A use case for idle handles is discussed in utilities. Let us use an idle watcher to look at the watcher life cycle and see how uv_run() will now block because a watcher is present. The idle watcher is stopped when the count is reached and uv_run() exits since no event watchers are active.

#include <stdio.h>
#include <uv.h>

int64_t counter = 0;

void wait_for_a_while(uv_idle_t* handle) {
    counter++;

    if (counter >= 10e6)
        uv_idle_stop(handle);
}

int main() {
    uv_idle_t idler;

    uv_idle_init(uv_default_loop(), &idler);
    uv_idle_start(&idler, wait_for_a_while);

    printf("Idling...\n");
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);

    uv_loop_close(uv_default_loop());
    return 0;
}

Storing context

In callback based programming style you’ll often want to pass some ‘context’ –application specific information – between the call site and the callback. All handles and requests have a void* data member which you can set to the context and cast back in the callback. This is a common pattern used throughout the C library ecosystem. In addition uv_loop_t also has a similar data member.

Advanced event loops

libuv provides considerable user control over event loops, and you can achieve interesting results by juggling multiple loops. You can also embed libuv’s event loop into another event loop based library – imagine a Qt based UI, and Qt’s event loop driving a libuv backend which does intensive system level tasks.

Stopping an event loop

uv_stop() can be used to stop an event loop. The earliest the loop will stop running is on the next iteration, possibly later. This means that events that are ready to be processed in this iteration of the loop will still be processed, so uv_stop() can’t be used as a kill switch. When uv_stop() is called, the loop won’t block for i/o on this iteration. The semantics of these things can be a bit difficult to understand, so let’s look at uv_run() where all the control flow occurs.

src/unix/core.c - uv_run

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
  }
}

stop_flag is set by uv_stop(). Now all libuv callbacks are invoked within the event loop, which is why invoking uv_stop() in them will still lead to this iteration of the loop occurring. First libuv updates timers, then runs pending timer, idle and prepare callbacks, and invokes any pending I/O callbacks. If you were to call uv_stop() in any of them, stop_flag would be set. This causes uv_backend_timeout() to return 0, which is why the loop does not block on I/O. If on the other hand, you called uv_stop() in one of the check handlers, I/O has already finished and is not affected.

uv_stop() is useful to shutdown a loop when a result has been computed or there is an error, without having to ensure that all handlers are stopped one by one.

Here is a simple example that stops the loop and demonstrates how the current iteration of the loop still takes places.

uvstop/main.c

#include <stdio.h>
#include <uv.h>

int64_t counter = 0;

void idle_cb(uv_idle_t *handle) {
    printf("Idle callback\n");
    counter++;

    if (counter >= 5) {
        uv_stop(uv_default_loop());
        printf("uv_stop() called\n");
    }
}

void prep_cb(uv_prepare_t *handle) {
    printf("Prep callback\n");
}

int main() {
    uv_idle_t idler;
    uv_prepare_t prep;

    uv_idle_init(uv_default_loop(), &idler);
    uv_idle_start(&idler, idle_cb);

    uv_prepare_init(uv_default_loop(), &prep);
    uv_prepare_start(&prep, prep_cb);

    uv_run(uv_default_loop(), UV_RUN_DEFAULT);

    return 0;
}

Licenses and Attributions


Speak Your Mind

-->