In the last article, THE author raised several problems in understanding the Event Loop, Timers and process.nextTick in NodeJs. Now I give my understanding of these problems. Please correct any mistakes.

If you are interested in NodeJs, please visit S.E.L.D. or my wechat (w979436427) to discuss your node learning experience

When is the poll phase blocked?

As mentioned in the previous article, the poll phase “receives new I/O events and node blocks here when appropriate.” What happens when it blocks? How long is the blockage?

To solve this problem, we must dive into the source code of Libuv to see how the poll phase is implemented:

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);// This is the poll phase
    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */
  if(loop->stop_flag ! =0)
    loop->stop_flag = 0;

  return r;
}
Copy the code

Uv__io_poll takes a timeout, which determines the length of the poll block. Knowing this, we can turn the question to: what determines the value of timeout?

Back in the source code, timeout starts with a value of 0, which means that the poll phase goes directly to the check phase without blocking. But when (mode == UV_RUN_ONCE &&! Ran_pending) | | mode = = UV_RUN_DEFAULT these conditions was founded, the timeout is decided by uv_backend_timeout return values.

Here we need to interrupt the question about mode value. According to the official document, there are three modes of mode:

  • UV_RUN_DEFAULT
  • UV_RUN_ONCE
  • UV_RUN_NOWAIT

We only care about UV_RUN_DEFAULT here, because Node Event Loop uses this mode.

OK ~ back to the question, what does uv_backend_timeout return?

int uv_backend_timeout(const uv_loop_t* loop) {
  if(loop->stop_flag ! =0)
    return 0;

  if(! uv__has_active_handles(loop) && ! uv__has_active_reqs(loop))return 0;

  if(! QUEUE_EMPTY(&loop->idle_handles))return 0;

  if(! QUEUE_EMPTY(&loop->pending_queue))return 0;

  if (loop->closing_handles)
    return 0;

  return uv__next_timeout(loop);
}
Copy the code

This is a multi-step conditional judgment function, which we analyze one by one:

  1. If the Event loop has (or is) finished (called)uv_stop().stop_flag ! = 0),timeout0
  2. If there are no asynchronous tasks to deal with,timeout0
  3. If there are any unprocessedidle_handlesandpending_queue.timeoutIs 0 (foridle_handlesandpending_queueI have no idea what they represent, and I will update them in time if there is relevant information in the future.)
  4. If there are uncleared resources,timeout0
  5. If none of the preceding conditions are met, useuv__next_timeoutTo deal with
int uv__next_timeout(const uv_loop_t* loop) {
  const struct heap_node* heap_node;
  const uv_timer_t* handle;
  uint64_t diff;

  heap_node = heap_min((const struct heap*) &loop->timer_heap);
  if (heap_node == NULL)
    return- 1; /* block indefinitely */ handle = container_of(heap_node, uv_timer_t, heap_node);if (handle->timeout <= loop->time)
    return0; Diff = handle-> timeout-loop ->time; diff = handle-> timeout-loop ->time; // Cannot be greater than the maximum INT_MAXif (diff > INT_MAX)
    diff = INT_MAX;

  return diff;
}
Copy the code

To summarize, the poll phase blocks when the Event loop meets the following conditions:

  1. The event loop does not trigger the close action
  2. There are asynchronous queues left unprocessed
  3. All resources have been shut down

The maximum blocking time does not exceed the minimum threshold of a given timer

Why in a non-I /O loop,setTimeoutandsetImmediateIs the order of execution not necessarily?

SetTimeout and setImmediate Mediate Mediate Do not mediate in a non-I /O loop.

setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
Copy the code
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
Copy the code

Why is it that the same code is run twice with opposite results?

In node, setTimeout(cb, 0) === setTimeout(cb, 1)

During the first timers phase of an Event loop, Node compares a timer with loop->time with a minimum threshold. If the threshold is less than or equal to loop->time, the timer has expired and the corresponding callback is performed (the next timer is then checked). If not, it goes to the next stage.

So whether setTimeout is executed in the first phase depends on the size of loop->time. There are two possible cases:

  1. If loop->time >=1 is used, uv_run_timer takes effect, and timeout takes effect first

  2. If the preparation time before the first loop is less than 1ms and the current loop->time < 1, the first uv_run_timer in this loop does not take effect. Therefore, the uv_run_check command is executed immediately after the io_poll command. After the close CB is complete, the uv_run_timer is executed

That’s why the same piece of code executes randomly. An I/O callback must be executed immediately. Consider the following scenarios:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout((a)= > {
    console.log('timeout');
  }, 0);
  setImmediate((a)= > {
    console.log('immediate');
  });
});
Copy the code

Since the event registration for timeout and immediate is triggered when the readFile callback is executed, of course, The uv_run_timer in each event loop before the readFile callback will not be triggered by a timeout event, so when the readFile execution is complete and the POLL stage receives the fd event listening, the callback will be executed

  1. timeoutEvent registration
  2. immediateEvent registration
  3. Due to thereadFileThe callback from theuv_io_pollRun the command immediatelyuv_run_check, soimmediateThe event was executed
  4. The last of theuv_run_timerchecktimeoutEvent, executiontimeoutThe event

So you can see that both things registered in the I/O callback are always immediately executed first

What does it mean to expand the JS call stack?

Stack unwinding is the process of matching catch statements layer by layer after an exception is thrown. For example:

function a(){
  b();
}
function b(){
  c();
}
function c(){
  throw new Error('from function c');
}

a();
Copy the code

In this example, function C throws an exception, which first checks the c function itself to see if there is a try-related catch statement. If not, it exits the current function, frees the memory of the current function and destroys local objects, and continues the search in function B. This process is called stack unwinding.

reference

  1. zhuanlan.zhihu.com/p/35039878
  2. Cnodejs.org/topic/57d68…
  3. GNGSHN. Making. IO / 2017/09/01 /…
  4. Docs.libuv.org/en/v1.x/des…