Event Loop

Node.js belongs to the single-threaded event loop architecture. The event loop is implemented by the uv_run function of Libuv. The while loop is executed in this function, and then the event callbacks of each phase are continuously processed.

The processing of the event loop is equivalent to a consumer, consuming tasks generated by various codes. After Node.js is initialized, it begins to fall into the event loop, and the end of the event loop also means the end of Node.js. Let's take a look at the core code of the event loop.

    int uv_run(uv_loop_t* loop, uv_run_mode mode) {
      int timeout;
      int r;
      int ran_pending;
      // Submit the task to loop before uv_run
      r = uv__loop_alive(loop);
      // The event loop has no tasks to execute and is about to exit. Set the time of the current loop if (!r)
        uv__update_time(loop);
      // Exit the event loop if there is no task to process or uv_stop is called while (r != 0 && loop->stop_flag == 0) {
        // Update the time field of loop uv__update_time(loop);
        // Execute timeout callback uv__run_timers(loop);
        /*
          Execute the pending callback, ran_pending represents whether the pending queue is empty,
           i.e. no node can execute */
        ran_pending = uv__run_pending(loop);
        // Continue to execute various queues uv__run_idle(loop);
        uv__run_prepare(loop);

        timeout = 0;
        /*
          When the execution mode is UV_RUN_ONCE, if there is no pending node,
          Only blocking Poll IO, the default mode is also */
        if ((mode == UV_RUN_ONCE && !ran_pending) ||
              mode == UV_RUN_DEFAULT)
          timeout = uv_backend_timeout(loop);
        // Poll IO timeout is the timeout of epoll_wait uv__io_poll(loop, timeout);
         // Process the check phase uv__run_check(loop);
         // handle the close phase uv__run_closing_handles(loop);
        /*
          There is also a chance to execute the timeout callback, because uv__io_poll may return because the timer expired.
        */
        if (mode == UV_RUN_ONCE) {
          uv__update_time(loop);
          uv__run_timers(loop);
        }

        r = uv__loop_alive(loop);
        /*
          Execute only once, exit the loop, UV_RUN_NOWAIT means that it will not block in the Poll IO stage and the loop will only execute once */
        if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
          break;
      }
      // exited because uv_stop was called, reset the flag
      if (loop->stop_flag != 0)
        loop->stop_flag = 0;
      /*
        Returns whether there are still active tasks (handle or request),
        The agent can execute uv_run again
      */
      return r;
    }

Libuv is divided into several stages. The following is from first to last, and the relevant codes of each stage are analyzed separately.

3.1 Event loop timer In Libuv

the timer stage is the first stage to be processed. The timer is implemented as a min heap, and the node that expires the fastest is the root node. Libuv caches the current time at the beginning of each event loop.

In each round of the event loop, the cached time is used. When necessary, Libuv will explicitly update this time, because the operation needs to be called to obtain the time. The interface provided by the system, and frequently calling the system call will bring a certain amount of time. The cache time can reduce the calls of the operating system and improve the performance.

After Libuv caches the current latest time, it executes uv__run_timers, which traverses the minimum heap and finds the current timeout node. Because the nature of the heap is that the parent node is definitely smaller than the child. And the root node is the smallest, so if a root node, it doesn't time out, the following nodes also don't time out. For the node that times out, its callback is executed. Let's look at the specific logic.

    void uv__run_timers(uv_loop_t* loop) {
      struct heap_node* heap_node;
      uv_timer_t* handle;
      // Traverse the binary heap for (;;) {
        // Find the smallest node heap_node = heap_min(timer_heap(loop));
        // if not exit if (heap_node == NULL)
          break;
        // Find the first address of the structure through the structure field handle = container_of(heap_node, uv_timer_t, heap_node);
        // The smallest node does not have a supermarket, and the subsequent nodes will not time out if (handle->timeout > loop->time)
          break;
        // delete the node uv_timer_stop(handle);
        /*
          Retry inserting into the binary heap, if necessary (repeat is set, such as setInterval)
        */
        uv_timer_again(handle);
        // Execute callback handle->timer_cb(handle);
      }
    }

After executing the callback, there are two key operations, the first is stop, and the second is again. The logic of stop is very simple, that is, delete the handle from the binary heap and modify the state of the handle.

So what is again? again is to support the scenario of setInterval. If the handle is set with the repeat flag, the handle will continue to execute the timeout callback after every repeat time after the handle times out. For setInterval, the timeout is x, and the callback is executed after every x time.

This is the underlying principle of timers in Node.js. But Node.js does not insert a node into the min heap every time setTimeout/setInterval is adjusted. In Node.js, there is only one handle about uv_timer_s, which maintains a data structure in the JS layer, and calculates the earliest expiration every time. node, and then modify the timeout time of the handle, which is explained in the timer chapter.

In addition, the timer stage is also related to the Poll IO stage, because Poll IO may cause the main thread to block. In order to ensure that the main thread can execute the timer callback as soon as possible, Poll IO cannot block all the time, so at this time, the blocking time is the fastest The duration of the timer node of the period (for details, please refer to the uv_backend_timeout function in libuv core.c).

3.2 pending stage

The official website's explanation of the pending stage is that the IO callbacks that were not executed in the Poll IO stage of the previous round will be executed in the pending stage of the next round of loops.

From the source code point of view, when processing tasks in the Poll IO stage, in some cases, if the currently executed operation fails, a callback function needs to be executed to notify the caller of some information.

The callback function will not be executed immediately, but will be pending in the next round of the event loop. Stage execution (such as successful writing of data, or callback to the C++ layer when the TCP connection fails), let's first look at the processing of the pending stage.

    static int uv__run_pending(uv_loop_t* loop) {
      QUEUE* q;
      QUEUE pq;
      uv__io_t* w;

      if (QUEUE_EMPTY(&loop->pending_queue))
        return 0;
      // Move the node of the pending_queue queue to pq, that is, clear the pending_queue
      QUEUE_MOVE(&loop->pending_queue, &pq);

      // Traverse the pq queue while (!QUEUE_EMPTY(&pq)) {
        // Take out the current first node to be processed, ie pq.next
        q = QUEUE_HEAD(&pq);
        // Remove the current node to be processed from the queue QUEUE_REMOVE(q);
        /*
          Reset the prev and next pointers, because at this time these two pointers point to the two nodes in the queue */
        QUEUE_INIT(q);
        w = QUEUE_DATA(q, uv__io_t, pending_queue);
        w->cb(loop, w, POLLOUT);
      }

      return 1;
    }

The processing logic of the pending phase is to execute the nodes in the pending queue one by one. Let's take a look at how the nodes of the pending queue are produced.

    void uv__io_feed(uv_loop_t* loop, uv__io_t* w) {
      if (QUEUE_EMPTY(&w->pending_queue))
        QUEUE_INSERT_TAIL(&loop->pending_queue, &w->pending_queue);
    }

Libuv generates pending tasks through the uvio_feed function. From the Libuv code, we will call this function when we see IO errors (such as the uvtcp_connect function of tcp.c).

    if (handle->delayed_error)
        uv__io_feed(handle->loop, &handle->io_watcher);

After the data is written successfully (such as TCP, UDP), a node is also inserted into the pending queue, waiting for a callback. For example, the code executed after sending data successfully (uv__udp_sendmsg function of udp.c)

    // Move out of the write queue after sending QUEUE_REMOVE(&req->queue);
    // Join the write completion queue QUEUE_INSERT_TAIL(&handle->write_completed_queue, &req->queue);
    /*
      After some node data is written, insert the IO observer into the pending queue,
      Execute callback in pending stage */
    uv__io_feed(handle->loop, &handle->io_watcher);

When the IO is finally closed (such as closing a TCP connection), the corresponding node will be removed from the pending queue. Because it has been closed, naturally there is no need to execute the callback.

    void uv__io_close(uv_loop_t* loop, uv__io_t* w) {
      uv__io_stop(loop,
                    w,
                    POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI);
      QUEUE_REMOVE(&w->pending_queue);
    }

3.3 prepare, check, idle of event loop

prepare, check, and idle are relatively simple stages in the Libuv event loop, and their implementations are the same (see loop-watcher.c). This section only explains the prepare stage. We know that Libuv is divided into handle and request, and the tasks of the prepare stage belong to the handle type. This means that nodes in the prepare phase are executed every time the event loop occurs unless we explicitly remove them. Let's first see how to use it.

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

    int main() {
        uv_prepare_t prep;
        // Initialize a handle, uv_default_loop is the core structure of the event loop uv_prepare_init(uv_default_loop(), &prep);
            // Register handle callback uv_prepare_start(&prep, prep_cb);
            // Start the event loop uv_run(uv_default_loop(), UV_RUN_DEFAULT);
        return 0;
    }

When the main function is executed, Libuv will execute the callback prep_cb in the prepare phase. Let's analyze this process.

    int uv_prepare_init(uv_loop_t* loop, uv_prepare_t* handle) {
        uv__handle_init(loop, (uv_handle_t*)handle, UV_PREPARE);
        handle->prepare_cb = NULL;
        returnThe node is removed from the current queue QUEUE_REMOVE(q);
           // Reinsert the original queue QUEUE_INSERT_TAIL(&loop->prepare_handles, q);
           // Execute the callback function h->prepare_cb(h);
        }
      }

The logic of the uv__run_prepare function is very simple, but one key point is that after each node is executed, Libuv will re-insert the node into the queue, so the nodes in the prepare (including idle, check) stage will be executed in each round of the event loop. implement. Nodes such as timer, pending, and closing stages are one-time and will be removed from the queue after being executed. Let's review the test code at the beginning.

Because it sets the operating mode of Libuv to be the default mode. The prepare queue always has a handle node, so it will not exit. It will always execute the callback. So what if we want to quit? Or do not execute a node of the prepare queue. We just need to stop it once.

       int uv_prepare_stop(uv_prepare_t* handle) {
        if (!uv__is_active(handle)) return 0;
        // Remove the handle from the prepare queue, but also mount it in handle_queue QUEUE_REMOVE(&handle->queue);
         // Clear the active flag bit and subtract the active number of handles in the loop uv__handle_stop(handle);
        return 0;
      }

The stop function and the start function have opposite functions, which is the principle of the prepare, check, and idle phases in Node.js.

3.4 Poll IO of event loop

Poll IO is a very important stage of Libuv. File IO, network IO, signal processing, etc. are all processed in this stage, which is also the most complicated stage. The processing logic is in the uv**io_poll function of core.c. This function is more complicated, so we analyze it separately. Before starting to analyze Poll IO, let's take a look at some of the data structures related to it.

  1. IO observer uv**io_t. This structure is the core structure of the Poll IO stage. It mainly saves IO-related file descriptors, callbacks, events of interest and other information.
  2. watcher_queue watcher queue. All IO observers that need to be processed by Libuv are mounted in this queue, and Libuv will be processed one by one in the Poll IO stage.

Next we start to analyze the Poll IO stage. Look at the first paragraph of logic.

     // If there is no IO observer, return directly if (loop->nfds == 0) {
        assert(QUEUE_EMPTY(&loop->watcher_queue));
        return;
      }
      // Traverse the IO watcher queue while (!QUEUE_EMPTY(&loop->watcher_queue)) {
          // Take out the current head node q = QUEUE_HEAD(&loop->watcher_queue);
        // Dequeue QUEUE_REMOVE(q);
        // Initialize (reset) the front and back pointers of the node QUEUE_INIT(q);
        // Successfully obtain the first address of the structure through the structure w = QUEUE_DATA(q, uv__io_t, watcher_queue);
        // Set the current event of interest e.events = w->pevents;
        /*
              The fd field is used here, and after the event is triggered, the fd is used from the watches
              The corresponding IO observer is found in the field, and there is no plan to use ptr to point to the IO observer*/
        e.data.fd = w->fd;
        // If w->events is 0 when initialized, add it, otherwise modify if (w->events == 0)
          op = EPOLL_CTL_ADD;
        else
          op = EPOLL_CTL_MOD;
        // Modify the data of epoll epoll_ctl(loop->backend_fd, op, w->fd, &e)
        // Record the current state when added to epoll w->events = w->pevents;
      }

The first step is to traverse the IO observer and modify the data of epoll. Then get ready to go into wait.

      psigset = NULL;
     if (loop->flags & UV_LOOP_BLOCK_SIGPROF) {
       sigemptyset(&sigset);
       sigaddset(&sigset, SIGPROF);
       psigset = &sigset;
     }
       /*
        http://man7.org/Linux/man-pages/man2/epoll_wait.2.html
        pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
        ready = epoll_wait(epfd, &events, maxevents, timeout);
        pthread_sigmask(SIG_SETMASK, &origmask, NULL);
        That is to shield the SIGPROF signal to prevent the SIGPROF signal from waking up epoll_wait, but there is no ready event*/
       nfds = epoll_pwait(loop->backend_fd,
                          events,
                          ARRAY_SIZE(events),
                          timeout,
                          psigset);
       // epoll may block, here you need to update the time of the event loop uv__update_time(loop) ```

epoll_wait may cause the main thread to block, so the current time needs to be updated after wait returns, otherwise the time difference will be relatively large when used, because Libuv will cache the current time value at the beginning of each round of time loop. Use it directly in other places, instead of getting it every time. Next, let's look at the processing after epoll returns (assuming an event is triggered).

       // Save some data returned by epoll_wait, maybe_resize +2 when applying for space loop->watchers[loop->nwatchers] = (void*) events;
       loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
       for (i = 0; i < nfds; i++) {
         // Triggered events and file descriptors pe = events + i;
         fd = pe->data.fd;
         // Get IO watchers according to fd, see the figure above w = loop->watchers[fd];
         // will be deleted in other callbacks, then delete from epoll if (w == NULL) {
           epoll_ctl(loop->backend_fd, EPOLL_CTL_DEL, fd, pe);
           continue;
         }
         if (pe->events != 0) {
            /*
                The event of interest to the IO observer for signal handling is fired,
                That is, a signal occurs.
            */
           if (w == &loop->signal_io_watcher)
             have_signals =
    }

After doing different processing according to different handles, then execute uv__make_close_pending to add nodes to the close queue.

    // The head insertion method is inserted into the closing queue and executed during the closing phase void uv__make_close_pending(uv_handle_t* handle) {
      handle->next_closing = handle->loop->closing_handles;
      handle->loop->closing_handles = handle;
    }

It is then processed one by one in the close phase. Let's take a look at the processing logic of the close phase

// Callback for executing closing phase static void uv\_\_run_closing_handles(uv_loop_t* loop) {
 uv_handle_t* p;
 uv_handle_t\* q;

       p = loop->closing_handles;
       loop->closing_handles = NULL;

       while (p) {
         q = p->next_closing;
         uv__finish_close(p);
         p = q;
       }
     }

     // Execute the callback of the closing phase static void uv__finish_close(uv_handle_t* handle) {
       handle->flags |= UV_HANDLE_CLOSED;
       ...
       uv__handle_unref(handle);
         // remove QUEUE_REMOVE(&handle->handle_queue) from handle queue;
       if (handle->close_cb) {
         handle->close_cb(handle);
       }
     }

uv__run_closing_handles will execute the callback of each task node one by one.

3.6 Controlling the event loop

Libuv uses the uv__loop_alive function to determine whether the event loop needs to continue to execute. Let's look at the definition of this function.

    static int uv__loop_alive(const uv_loop_t* loop) {
      return uv__has_active_handles(loop) ||
             uv__has_active_reqs(loop) ||
             loop->closing_handles != NULL;
    }

Why is there a judgment of closing_handle? Judging from the code of uv_run, after the close phase is executed, uv_loop_alive will be executed immediately. Normally, the queue in the close phase is empty, but if we add a new node to the close queue in the close callback, and the node It will not be executed in the close phase of this round, which will cause the close phase to be executed, but there are still nodes in the close queue. If it exits directly, the corresponding callback cannot be executed. We see three cases where Libuv considers the event loop to be alive. If we control these three conditions, we can control the exit of the event loop. Let us understand this process with an example.

    const timeout = setTimeout(() => {
      console.log('never console')
    }, 5000);
    timeout.unref();

In the above code, the callback of setTimeout will not be executed. Unless the timeout period is very short, it will expire when the first round of event loop is short, otherwise after the first round of event loop, due to the influence of unref, the event loop exits directly. Unref affects the handle condition. The event loop code is as follows.

    while (r != 0 && loop->stop_flag == 0) {
        uv__update_time(loop);
        uv__run_timers(loop);
        // ...
        // uv__loop_alive returns false and jumps out of the while, thereby exiting the event loop r = uv__loop_alive(loop);
    }