【问题标题】:Does the nodejs (libuv) event loop execute all the callbacks in one phase (queue) before moving to the next or run in a round robin fashion?nodejs(libuv)事件循环是否在一个阶段(队列)中执行所有回调,然后再移动到下一个阶段或以循环方式运行?
【发布时间】:2020-06-17 16:04:15
【问题描述】:

我正在研究 Node.js 中 libuv 提供的事件循环。我遇到了following blog by Deepal Jayasekara,也看到了 youtube 上 Bert Belder 和 Daniel Khan 的解释。

有一点我不清楚-根据我的理解,事件循环在进入另一个阶段之前会处理一个阶段的所有项目。因此,如果是这种情况,如果 setTimeout 阶段不断添加回调,我应该能够阻止事件循环。

但是,当我尝试复制它时 - 它没有发生。以下是代码:

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.write('Hello World!');
  console.log("Response sent");
  res.end();
}).listen(8081);


setInterval(() => {
  console.log("Entering for loop");
// Long running loop that allows more callbacks to get added to the setTimeout phase before this callback's processing completes
 for (let i = 0; i < 7777777777; i++); 
 console.log("Exiting for loop");
}, 0);

事件循环似乎以循环方式运行。它首先执行在我向服务器发送请求之前添加的回调,然后处理请求,然后继续回调。感觉就像一个队列正在运行。 据我了解,没有一个队列,所有过期的计时器回调都应该在进入下一阶段之前首先执行。因此上面的 sn-p 应该不能返回 Hello World 响应。

对此有什么可能的解释? 谢谢。

【问题讨论】:

  • 仅供参考,定时器的管理方式与其他类型的回调完全不同。计时器系统是事件循环本身的特定部分。
  • 这篇文章可能会有所帮助:Understanding the Node.js event loop phases and how it executes the JavaScript code,尤其是“投票”阶段的描述。
  • @jfriend00 事件循环在轮询阶段等待,直到将过期的计时器回调添加到计时器队列/堆中。但是一旦它开始处理计时器回调,它将耗尽整个队列,并且由于我不断添加它,所以循环应该会卡住。除非...在事件循环开始处理之后添加到队列中的回调直到下一次迭代才被考虑。那我想应该是有道理的。或者,如果 http.get() 方法在内部使用了 Promise 或 nextTick。我会对此进行更多研究。很想听听您的意见。
  • 我认为计时器到期时不会被添加到队列中。相反,当它在事件循环中时,nodejs 会检查哪个计时器的时间已经过去。这将允许他们仅处理在此事件循环周期开始时触发的计时器,从而使调度更加公平。如果你想在这里了解真正的细节,你必须去看看这部分事件循环的 libuv 代码。
  • 好的,我挖了libuv代码,找到了事件循环代码,找到了定时器处理代码,贴出一个答案来解释一下。

标签: javascript node.js event-loop


【解决方案1】:

如果你查看 libuv 本身,你会发现在事件循环中运行计时器的操作部分是函数uv_run_timers()

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
    heap_node = heap_min(timer_heap(loop));
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time)
      break;

    uv_timer_stop(handle);
    uv_timer_again(handle);
    handle->timer_cb(handle);
  }
}

它的工作方式是事件循环在当前时间设置一个时间标记,然后它一个接一个地处理到该时间到期的所有计时器,而不更新循环时间。因此,这将触发所有已经过期的计时器,但不会触发任何到期的新计时器,同时它正在处理已经到期的计时器。

这导致更公平的调度,因为它运行所有到期的计时器,然后运行并运行事件循环中的其余类型的事件,然后返回执行任何更多到期的计时器。这将不会处理在此事件循环周期开始时未到期的任何计时器,但在处理其他计时器时到期。因此,您会看到您询问的行为。

上面的函数是从事件循环的main part调用的,代码如下:

int uv_run(uv_loop_t *loop, uv_run_mode mode) {
  DWORD 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);                    <==  establish loop time
    uv__run_timers(loop);                    <==  process only timers due by that loop time

    ran_pending = uv_process_reqs(loop);
    uv_idle_invoke(loop);
    uv_prepare_invoke(loop);

 .... more code here

}

注意在调用uv__run_timers() 之前对uv_update_time(loop) 的调用。这设置了uv__run_timers() 引用的计时器。这是the code 代表uv_update_time()

void uv_update_time(uv_loop_t* loop) {
  uint64_t new_time = uv__hrtime(1000);
  assert(new_time >= loop->time);
  loop->time = new_time;
}

【讨论】:

  • 超级!谢谢:)
【解决方案2】:

来自docs

当事件循环进入一个 给定阶段,它将执行特定于该阶段的任何操作, 然后在该阶段的队列中执行回调,直到队列被 已用尽或已执行最大数量的回调。当。。。的时候 队列已用尽或达到回调限制,事件 循环将进入下一个阶段,依此类推。

同样来自docs

当延迟大于2147483647或小于1时,延迟设置为1

现在,当您运行 sn-p 时,会发生以下事情,

  1. 脚本执行开始,回调注册到特定阶段。此外,正如文档所暗示的,setInterval 延迟隐式转换为 1 秒。
  2. 1 秒后,您的setInterval 回调将被执行,它将阻塞事件循环,直到所有迭代并完成。同时,至少在循环终止之前,不会通知 eventloop 任何传入请求。
  3. 一旦完成所有迭代,并且有 1 秒的超时,轮询阶段将执行您的 HTTP 请求回调(如果有)。
  4. 返回第 2 步。

【讨论】:

    猜你喜欢
    • 2021-10-07
    • 2017-06-01
    • 2018-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-03
    • 2019-03-06
    • 1970-01-01
    相关资源
    最近更新 更多