Node의 이벤트 루프 및 process.nextTick() 이해

青灯夜游
풀어 주다: 2022-03-11 20:14:11
앞으로
2186명이 탐색했습니다.

이 글은 Nodejs의 이벤트 루프에 대한 이해를 돕고, 이벤트 루프 메커니즘, process.nextTick() 등을 분석합니다. 모두에게 도움이 되기를 바랍니다!

Node의 이벤트 루프 및 process.nextTick() 이해

이벤트 루프란 무엇입니까

이벤트 루프는 Node.js가 비차단 I/O 작업을 처리하는 메커니즘입니다. JavaScript가 단일 스레드임에도 불구하고 가능한 경우 시스템에 작업을 오프로드합니다. 커널.

이제 대부분의 코어는 멀티스레드이므로 백그라운드에서 여러 작업을 처리할 수 있습니다. 작업 중 하나가 완료되면 커널은 Node.js에 적절한 콜백 함수를 폴링 대기열에 추가하고 실행을 기다리도록 알립니다. 이 글의 뒷부분에서 자세히 소개하겠습니다.

이벤트 루프 메커니즘 분석

Node.js가 시작되면 이벤트 루프를 초기화하고 제공된 입력 스크립트를 처리하며(또는 이 문서에서 다루지 않는 REPL에 넣습니다) 호출할 수 있습니다. 일부 비동기 API를 사용하거나 타이머를 예약하거나 process.nextTick()을 호출하고 이벤트 루프 처리를 시작하세요. process.nextTick(),然后开始处理事件循环。

下面的图表展示了事件循环操作顺序的简化概览。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
로그인 후 복사

注意:每个框被称为事件循环机制的一个阶段。

每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。

由于这些操作中的任何一个都可能调度_更多的_操作和由内核排列在轮询阶段被处理的新事件, 且在处理轮询中的事件时,轮询事件可以排队。因此,长时间运行的回调可以允许轮询阶段运行长于计时器的阈值时间。有关详细信息,请参阅 计时器轮询 部分。

注意: 在 Windows 和 Unix/Linux 实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上有七或八个步骤,但我们关心的是 Node.js 实际上使用以上的某些步骤。

阶段概述

  • 定时器:本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on(&#39;close&#39;, ...)
  • 아래 다이어그램은 이벤트 루프의 작업 순서에 대한 간략한 개요를 보여줍니다.
const fs = require(&#39;fs&#39;);

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile(&#39;/path/to/file&#39;, callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});
로그인 후 복사
로그인 후 복사

참고: 각 상자를 이벤트 루프 메커니즘의 단계라고 합니다.

각 단계에는 콜백을 실행하기 위한 FIFO 대기열이 있습니다. 각 단계는 특별하지만 일반적으로 이벤트 루프가 특정 단계에 들어갈 때 해당 단계에 특정한 작업을 수행한 다음 대기열이 소진되거나 최대 콜백 수가 실행될 때까지 해당 단계의 대기열에서 콜백을 실행합니다. 대기열이 소진되거나 콜백 제한에 도달하면 이벤트 루프가 다음 단계로 이동합니다. 이러한 작업 중 하나라도

폴링

단계에서 처리되도록 커널에 의해 대기열에 추가된 작업과 새 이벤트를 예약할 수 있고 폴링 단계에서 이벤트를 처리하는 동안 폴링 이벤트가 대기열에 포함될 수 있습니다. 따라서 장기 실행 콜백을 사용하면 폴링 단계가 타이머의 임계값 시간보다 오래 실행될 수 있습니다. 자세한 내용은 Timer

Polling 부분. 참고: Windows와 Unix/Linux 구현 간에는 미묘한 차이가 있지만 이는 데모에서는 중요하지 않습니다. 가장 중요한 부분은 여기에 있습니다. 실제로는 7~8개의 단계가 있지만 우리가 관심을 갖는 것은 Node.js가 실제로 위의 단계 중 일부를 사용한다는 것입니다.

단계 개요

  • 🎜Timer🎜: 이 단계의 실행은 setTimeout()에 의해 제어되었습니다. setInterval() 의 스케줄링 콜백 함수. 🎜
  • 🎜보류 중인 콜백🎜: 다음 루프 반복까지 실행이 지연되는 I/O 콜백입니다. 🎜
  • 🎜idle, prepare🎜: 시스템 내부에서만 사용됩니다. 🎜
  • 🎜Polling🎜: 새 I/O 이벤트 검색, I/O 관련 콜백 실행(거의 모든 경우, 타이머 및 setImmediate()</ code>에 의해 처리되는 종료 콜백 제외) 다른 경우 노드는 적절한 시간에 여기에서 차단됩니다. 🎜<li>🎜Detection🎜: <code>setImmediate() 여기서 콜백 함수가 실행됩니다. 🎜
  • 🎜닫힌 콜백 함수🎜: socket.on('close', ...)와 같은 일부 닫힌 콜백 함수. 🎜🎜🎜각 이벤트 루프 실행 사이에 Node.js는 비동기 I/O 또는 타이머를 기다리고 있는지 확인하고 그렇지 않은 경우 완전히 종료됩니다. 🎜🎜단계에 대한 자세한 개요 🎜🎜🎜타이머 🎜🎜🎜타이머는 사용자가 실행하기를 원하는 정확한 시간이 아니라 제공된 콜백이 실행될 수 있는 🎜임계값🎜을 지정합니다. 지정된 간격이 지나면 타이머 콜백이 최대한 빨리 실행됩니다. 그러나 운영 체제 예약이나 기타 실행 중인 콜백으로 인해 지연될 수 있습니다. 🎜🎜🎜🎜Note🎜: 🎜🎜polling🎜 단계🎜는 타이머가 실행되는 시기를 제어합니다. 🎜🎜🎜예를 들어, 100밀리초 후에 시간 초과되는 타이머를 예약한 다음 스크립트가 95밀리초가 걸리는 비동기식으로 파일 읽기를 시작한다고 가정해 보겠습니다. 🎜
    const fs = require(&#39;fs&#39;);
    
    function someAsyncOperation(callback) {
      // Assume this takes 95ms to complete
      fs.readFile(&#39;/path/to/file&#39;, callback);
    }
    
    const timeoutScheduled = Date.now();
    
    setTimeout(() => {
      const delay = Date.now() - timeoutScheduled;
    
      console.log(`${delay}ms have passed since I was scheduled`);
    }, 100);
    
    // do someAsyncOperation which takes 95 ms to complete
    someAsyncOperation(() => {
      const startCallback = Date.now();
    
      // do something that will take 10ms...
      while (Date.now() - startCallback < 10) {
        // do nothing
      }
    });
    로그인 후 복사
    로그인 후 복사

    当事件循环进入 轮询 阶段时,它有一个空队列(此时 fs.readFile() 尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它等待 95 毫秒过后时,fs.readFile() 完成读取文件,它的那个需要 10 毫秒才能完成的回调,将被添加到 轮询 队列中并执行。当回调完成时,队列中不再有回调,因此事件循环机制将查看最快到达阈值的计时器,然后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。

    注意:为了防止 轮询 阶段饿死事件循环,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个硬性最大值(依赖于系统)。

    挂起的回调函数

    此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。

    轮询

    轮询 阶段有两个重要的功能:

    • 计算应该阻塞和轮询 I/O 的时间。

    • 然后,处理 轮询 队列里的事件。

    当事件循环进入 轮询 阶段且_没有被调度的计时器时_,将发生以下两种情况之一:

    • 如果 轮询 队列 不是空的

      ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。

    • 如果 轮询 队列 是空的,还有两件事发生:

      • 如果脚本被 setImmediate() 调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。

      • 如果脚本 未被 setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。

    一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。

    检查阶段

    此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。

    setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。

    通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。

    关闭的回调函数

    如果套接字或处理函数突然关闭(例如 socket.destroy()),则&#39;close&#39; 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。

    setImmediate() 对比 setTimeout()

    setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

    • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
    • setTimeout() 在最小阈值(ms 单位)过后运行脚本。

    执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。

    例如,如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:

    // timeout_vs_immediate.js
    setTimeout(() => {
      console.log(&#39;timeout&#39;);
    }, 0);
    
    setImmediate(() => {
      console.log(&#39;immediate&#39;);
    });
    
    
    $ node timeout_vs_immediate.js
    timeout
    immediate
    
    $ node timeout_vs_immediate.js
    immediate
    timeout
    로그인 후 복사

    但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:

    // timeout_vs_immediate.js
    const fs = require(&#39;fs&#39;);
    
    fs.readFile(__filename, () => {
      setTimeout(() => {
        console.log(&#39;timeout&#39;);
      }, 0);
      setImmediate(() => {
        console.log(&#39;immediate&#39;);
      });
    });
    
    
    $ node timeout_vs_immediate.js
    immediate
    timeout
    
    $ node timeout_vs_immediate.js
    immediate
    timeout
    로그인 후 복사

    使用 setImmediate() 相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关

    process.nextTick()

    理解 process.nextTick()

    您可能已经注意到 process.nextTick() 在图示中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick() 从技术上讲不是事件循环的一部分。相反,它都将在当前操作完成后处理 nextTickQueue, 而不管事件循环的当前阶段如何。这里的一个_操作_被视作为一个从底层 C/C++ 处理器开始过渡,并且处理需要执行的 JavaScript 代码。

    回顾我们的图示,任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。

    为什么会允许这样?

    为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:

    function apiCall(arg, callback) {
      if (typeof arg !== &#39;string&#39;)
        return process.nextTick(
          callback,
          new TypeError(&#39;argument should be string&#39;)
        );
    }
    로그인 후 복사

    代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 API 进行了更新,允许传递参数给 process.nextTick(),这将允许它接受任何在回调函数位置之后的参数,并将参数传递给回调函数作为回调函数的参数,这样您就不必嵌套函数了。

    我们正在做的是将错误传回给用户,但仅在执行用户的其余代码之后。通过使用process.nextTick(),我们保证 apiCall() 始终在用户代码的其余部分_之后_和在让事件循环继续进行_之前_,执行其回调函数。为了实现这一点,JS 调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用 process.nextTick(),而不触碰 RangeError: 超过 V8 的最大调用堆栈大小 限制。

    这种设计原理可能会导致一些潜在的问题。 以此代码段为例:

    let bar;
    
    // this has an asynchronous signature, but calls callback synchronously
    function someAsyncApiCall(callback) {
      callback();
    }
    
    // the callback is called before `someAsyncApiCall` completes.
    someAsyncApiCall(() => {
      // since someAsyncApiCall has completed, bar hasn&#39;t been assigned any value
      console.log(&#39;bar&#39;, bar); // undefined
    });
    
    bar = 1;
    로그인 후 복사

    用户将 someAsyncApiCall() 定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给 someAsyncApiCall() 的回调是在事件循环的同一阶段内被调用,因为 someAsyncApiCall() 实际上并没有异步执行任何事情。结果,回调函数在尝试引用 bar,但作用域中可能还没有该变量,因为脚本尚未运行完成。

    通过将回调置于 process.nextTick() 中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有的变量、函数等。它还具有不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况。下面是上一个使用 process.nextTick() 的示例:

    let bar;
    
    function someAsyncApiCall(callback) {
      process.nextTick(callback);
    }
    
    someAsyncApiCall(() => {
      console.log(&#39;bar&#39;, bar); // 1
    });
    
    bar = 1;
    로그인 후 복사

    这又是另外一个真实的例子:

    const server = net.createServer(() => {}).listen(8080);
    
    server.on(&#39;listening&#39;, () => {});
    로그인 후 복사

    只有传递端口时,端口才会立即被绑定。因此,可以立即调用 &#39;listening&#39; 回调。问题是 .on(&#39;listening&#39;) 的回调在那个时间点尚未被设置。

    为了绕过这个问题,&#39;listening&#39; 事件被排在 nextTick() 中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。

    process.nextTick() 对比 setImmediate()

    就用户而言,我们有两个类似的调用,但它们的名称令人费解。

    • process.nextTick() 在同一个阶段立即执行。
    • setImmediate() 在事件循环的接下来的迭代或 'tick' 上触发。

    实质上,这两个名称应该交换,因为 process.nextTick()setImmediate() 触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。

    我们建议开发人员在所有情况下都使用 setImmediate(),因为它更容易理解。

    为什么要使用 process.nextTick()?

    有两个主要原因:

    1. 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。

    2. 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

    以下是一个符合用户预期的简单示例:

    const server = net.createServer();
    server.on(&#39;connection&#39;, (conn) => {});
    
    server.listen(8080);
    server.on(&#39;listening&#39;, () => {});
    로그인 후 복사

    假设 listen() 在事件循环开始时运行,但 listening 的回调被放置在 setImmediate() 中。除非传递过主机名,才会立即绑定到端口。为使事件循环继续进行,它必须命中 轮询 阶段,这意味着有可能已经接收了一个连接,并在侦听事件之前触发了连接事件。

    另一个示例运行的函数构造函数是从 EventEmitter 继承的,它想调用构造函数:

    const EventEmitter = require(&#39;events&#39;);
    const util = require(&#39;util&#39;);
    
    function MyEmitter() {
      EventEmitter.call(this);
      this.emit(&#39;event&#39;);
    }
    util.inherits(MyEmitter, EventEmitter);
    
    const myEmitter = new MyEmitter();
    myEmitter.on(&#39;event&#39;, () => {
      console.log(&#39;an event occurred!&#39;);
    });
    로그인 후 복사

    你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 process.nextTick() 来设置回调,以便在构造函数完成后发出该事件,这是预期的结果:

    const EventEmitter = require(&#39;events&#39;);
    const util = require(&#39;util&#39;);
    
    function MyEmitter() {
      EventEmitter.call(this);
    
      // use nextTick to emit the event once a handler is assigned
      process.nextTick(() => {
        this.emit(&#39;event&#39;);
      });
    }
    util.inherits(MyEmitter, EventEmitter);
    
    const myEmitter = new MyEmitter();
    myEmitter.on(&#39;event&#39;, () => {
      console.log(&#39;an event occurred!&#39;);
    });
    로그인 후 복사

    来源:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

    更多node相关知识,请访问:nodejs 教程

    위 내용은 Node의 이벤트 루프 및 process.nextTick() 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿