> 웹 프론트엔드 > JS 튜토리얼 > Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

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

Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

Node는 원래 고성능 웹 서버를 구축하기 위해 탄생했습니다. JavaScript용 서버 측 런타임으로 이벤트 기반, 비동기 I/O, 단일 스레딩과 같은 기능을 갖추고 있습니다. 이벤트 루프를 기반으로 한 비동기 프로그래밍 모델을 통해 Node는 높은 동시성을 처리하고 서버 성능을 크게 향상시키는 동시에 JavaScript의 단일 스레드 특성을 유지하므로 Node에서 상태 동기화 및 문제를 처리할 필요가 없습니다. 다중 스레드에서 교착 상태가 발생합니다. 스레드 컨텍스트 전환으로 인한 성능 오버헤드가 없습니다. 이러한 특성을 바탕으로 Node는 고성능, 높은 동시성이라는 본질적인 장점을 갖고 있으며, 이를 기반으로 고속, 확장 가능한 다양한 네트워크 응용 플랫폼을 구축할 수 있습니다.

이 기사는 노드 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 설명합니다. 도움이 되기를 바랍니다.

왜 비동기인가요?

Node가 핵심 프로그래밍 모델로 비동기식을 사용하는 이유는 무엇입니까?

앞서 언급했듯이 Node는 원래 고성능 웹 서버를 구축하기 위해 탄생했습니다. 비즈니스 시나리오에서 완료해야 할 여러 가지 관련 없는 작업 집합이 있다고 가정하면 두 가지 최신 주류 솔루션이 있습니다.

  • 단일 스레드가 연속적으로 실행됩니다. .

  • 여러 스레드를 병렬로 완료합니다.

단일 스레드 직렬 실행은 동기식 프로그래밍 모델이지만, I/O를 동기식으로 실행하기 때문에 프로그래머의 사고 방식에 더 부합하고 더 편리한 코드를 작성하기가 더 쉽습니다. 단일 요청은 한 번에 처리될 수 있으므로 서버의 응답 속도가 느려지고 동시성이 높은 애플리케이션 시나리오에서는 사용할 수 없습니다. 또한 I/O를 차단하므로 CPU는 항상 I/O가 완료될 때까지 기다립니다. 다른 작업을 수행할 수 없어 CPU의 성능이 저하됩니다. 처리 능력이 완전히 활용되지 않아 궁극적으로 효율성이 저하됩니다. 멀티 스레드 프로그래밍 모델은 상태 동기화 및 프로그래밍 교착 상태와 같은 문제로 인해 개발자에게 골치 아픈 문제를 안겨줍니다. 멀티스레딩은 멀티코어 CPU에서 CPU 활용도를 효과적으로 향상시킬 수 있습니다.

단일 스레드 직렬 실행과 다중 스레드 병렬 실행의 프로그래밍 모델에는 장점이 있지만 성능과 개발 난이도 측면에서 단점도 있습니다.

또한 클라이언트 요청에 대한 응답 속도부터 시작하여 클라이언트가 동시에 두 개의 리소스를 얻는 경우 동기 방식의 응답 속도는 두 리소스의 응답 속도의 합이 되고 응답 속도는 비동기식 방법은 두 가지가 될 것입니다. 그중 가장 큰 방법은 동기화에 비해 성능 이점이 매우 분명합니다. 애플리케이션 복잡성이 증가함에 따라 이 시나리오는 동시에 n개의 요청에 응답하도록 발전할 것이며 동기화에 비해 비동기의 장점이 강조될 것입니다.

요약하자면 Node는 다음과 같이 대답합니다. 단일 스레드를 사용하여 다중 스레드 교착 상태, 상태 동기화 및 기타 문제를 방지하고, 비동기 I/O를 사용하여 단일 스레드가 차단되지 않도록 하여 CPU를 더 잘 사용합니다. 이것이 Node가 비동기를 핵심 프로그래밍 모델로 사용하는 이유입니다.

또한 멀티 코어 CPU를 활용하지 못하는 단일 스레드의 단점을 보완하기 위해 Node는 브라우저에도 Web Worker와 유사한 하위 프로세스를 제공하여 작업자 프로세스를 통해 CPU를 효율적으로 활용할 수 있습니다.

비동기를 어떻게 구현하나요?

비동기를 사용해야 하는 이유에 대해 이야기한 후 비동기를 구현하는 방법은 무엇입니까?

우리가 일반적으로 부르는 비동기 작업에는 두 가지 유형이 있습니다. 하나는 파일 I/O 및 네트워크 I/O와 같은 I/O 관련 작업이고, 다른 하나는

처럼 I/O와 관련 없는 작업입니다. 분명히 우리가 말하는 비동기는 I/O와 관련된 작업, 즉 비동기 I/O를 의미합니다.

setTimeOutsetIntervalI/O 호출로 인해 후속 프로그램의 실행이 차단되지 않고 I/O가 완료될 때까지 기다리는 원래 시간이 실행을 위해 필요한 다른 비즈니스에 할당되기를 바라면서 비동기 I/O가 제안되었습니다. 이 목표를 달성하려면 Non-Blocking I/O가 필요합니다.

I/O 차단은 CPU가 I/O 호출을 시작한 후 I/O가 완료될 때까지 차단된다는 의미입니다. 차단 I/O를 알면 비차단 I/O는 이해하기 쉽습니다. CPU는 차단하고 기다리는 대신 I/O 호출을 시작한 후 즉시 반환됩니다. 분명히 차단 I/O에 비해 비차단 I/O의 성능이 더 향상되었습니다.

그러면 비차단 I/O가 사용되며 CPU는 I/O 호출 후 즉시 반환될 수 있으므로 I/O가 완료되었음을 어떻게 알 수 있습니까? 정답은 여론조사입니다.

적시에 I/O 호출 상태를 얻기 위해 CPU는 I/O 작업이 완료되었는지 확인하기 위해 지속적으로 I/O 작업을 반복적으로 호출합니다. 완료된 것을 폴링이라고 합니다.

물론 폴링을 하면 CPU가 상태 판단을 반복적으로 수행하게 되어 CPU 리소스가 낭비됩니다. 또한, 폴링 간격을 제어하기가 어렵습니다. 간격이 너무 길면 I/O 작업 완료 시 적시에 응답을 받지 못하므로 간격이 너무 짧으면 간접적으로 애플리케이션의 응답 속도가 느려집니다. 필연적으로 폴링에 CPU가 소비됩니다. 시간이 더 오래 걸리고 CPU 리소스 활용도가 줄어듭니다.

따라서 폴링은 비차단 I/O가 후속 프로그램의 실행을 차단하지 않는다는 요구 사항을 충족하지만 애플리케이션의 경우 여전히 일종의 동기화로만 간주될 수 있습니다. I/O를 완료하기 위해 Returning을 하는데 여전히 기다리는 시간이 많이 걸렸습니다.

우리가 기대하는 완벽한 비동기 I/O는 애플리케이션이 비차단 호출을 시작하는 것입니다. 대신 폴링을 통해 I/O 호출 상태를 지속적으로 쿼리할 필요가 없으며 다음 작업이 직접 처리될 수 있습니다. /O가 완료되면 세마포어나 콜백을 통해 데이터를 애플리케이션에 전달합니다.

이 비동기 I/O를 어떻게 구현하나요? 답은 스레드 풀입니다.

이 글에서는 Node가 단일 스레드에서 실행된다고 항상 언급했지만, 여기서 단일 스레드는 I/O 작업과 같이 주요 업무와 관련이 없는 부분의 경우 JavaScript 코드가 단일 스레드에서 실행된다는 의미입니다. logic, run 다른 스레드에서 구현하면 메인 스레드의 실행에 영향을 주거나 차단하지 않습니다. 반대로 메인 스레드의 실행 효율성을 향상시키고 비동기 I/O를 실현할 수 있습니다.

스레드 풀을 통해 메인 스레드는 I/O 호출만 수행하고, 다른 스레드는 차단 I/O 또는 비차단 I/O와 폴링 기술을 수행하여 데이터 수집을 완료한 다음 스레드 간 통신을 사용하여 전송합니다. I/O/O는 획득한 데이터를 전송하므로 비동기 I/O 구현이 쉽습니다.

Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

메인 스레드는 I/O 호출을 수행하고 스레드 풀은 I/O 작업을 수행하여 획득을 완료합니다. 그런 다음 이를 스레드 간에 전달합니다. 통신은 I/O 호출을 완료하기 위해 데이터를 메인 스레드로 전송합니다. 그런 다음 메인 스레드는 콜백 함수를 사용하여 데이터를 사용자에게 노출하고 사용자는 데이터를 사용하여 완료합니다. 이는 비즈니스 로직 수준의 I/O 호출입니다. 완전한 비동기 I/O 프로세스입니다. 사용자의 경우, 기본 레이어의 번거로운 구현 세부 사항에 신경 쓸 필요가 없습니다. 아래와 같이 Node에 캡슐화된 비동기 API를 호출하고 비즈니스 로직을 처리하는 콜백 함수를 전달하기만 하면 됩니다.

const fs = require("fs");

fs.readFile('example.js', (data) => {
  // 进行业务逻辑的处理
});
로그인 후 복사

Nodejs 구현 메커니즘은 플랫폼마다 다릅니다. Windows에서 IOCP는 주로 시스템 커널에 I/O 호출을 보내고 커널에서 완료된 I/O 작업을 가져오는 데 사용되며 이벤트 루프와 결합되어 완료됩니다. 비동기 I/O 프로세스는 Linux에서는 epoll을 통해, FreeBSD에서는 kqueue를 통해, Solaris에서는 이벤트 포트를 통해 구현됩니다. 스레드 풀은 Windows에서 커널(IOCP)에 의해 직접 제공되며, *nix 시리즈는 libuv 자체에 의해 구현됩니다. *nix 系列则由 libuv 自行实现。

由于 Windows 平台和 *nix 平台的差异,Node 提供了 libuv 作为抽象封装层,使得所有平台兼容性的判断都由这一层来完成,保证上层的 Node 与下层的自定义线程池及 IOCP 之间各自独立。Node 在编译期间会判断平台条件,选择性编译 unix 目录或是 win 目录下的源文件到目标程序中:

Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

以上就是 Node 对异步的实现。

(线程池的大小可以通过环境变量 UV_THREADPOOL_SIZE 设置,默认值为 4,用户可结合实际情况来调整这个值的大小。)

那么问题来了,在得到线程池传递过来的数据后,主线程是如何、何时调用回调函数的呢?答案是事件循环。

基于事件循环的异步编程模型

既然使用回调函数来进行对 I/O 数据的处理,就必然涉及到何时、如何调用回调函数的问题。在实际开发中,往往会涉及到多个、多类异步 I/O 调用的场景,如何合理安排这些异步 I/O 回调的调用,确保异步回调的有序进行是一个难题,而且,除了异步 I/O 之外,还存在定时器这类非 I/O 的异步调用,这类 API 实时性强,优先级相应地更高,如何实现不同优先级回调地调度呢?

因此,必须存在一个调度机制,对不同优先级、不同类型的异步任务进行协调,确保这些任务在主线程上有条不紊地运行。与浏览器一样,Node 选择了事件循环来承担这项重任。

Node 根据任务的种类和优先级将它们分为七类:Timers、Pending、Idle、Prepare、Poll、Check、Close。对于每类任务,都存在一个先进先出的任务队列来存放任务及其回调(Timers 是用小顶堆存放)。基于这七个类型,Node 将事件循环的执行分为如下七个阶段:

timers

这个阶段的执行优先级是最高的。

事件循环在这个阶段会检查存放定时器的数据结构(最小堆),对其中的定时器进行遍历,逐个比较当前时间和过期时间,判断该定时器是否过期,如果过期的话,就将该定时器的回调函数取出并执行。

pending

该阶段会执行网络、IO 等异常时的回调。一些 *nix

Windows 플랫폼과 *nix 플랫폼의 차이로 인해 Node는 libuv를 추상 캡슐화 계층으로 제공하므로 모든 플랫폼 호환성 판단은 이 계층에서 완료되어 상위 계층 Node와 하위 계층 노드는 사용자 정의 스레드 풀이고 IOCP는 서로 독립적입니다. 노드는 컴파일 중에 플랫폼 조건을 결정하고 unix 디렉터리 또는 win 디렉터리의 소스 파일을 대상 프로그램으로 선택적으로 컴파일합니다:

Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.

위는 Node의 비동기 구현입니다. 🎜🎜 (스레드 풀의 크기는 환경 변수 UV_THREADPOOL_SIZE를 통해 설정할 수 있습니다. 기본값은 4입니다. 이 값의 크기는 실제 상황에 따라 조정하면 됩니다.) 🎜🎜그러면 질문은 스레드 풀을 가져온 후 데이터를 전달한 후 메인 스레드가 콜백 함수를 언제 어떻게 호출합니까? 답은 이벤트 루프입니다. 🎜

이벤트 루프 기반 비동기 프로그래밍 모델

🎜콜백 함수는 I/O 데이터를 처리하는 데 사용되므로 콜백 함수를 호출하는 시기와 방법 문제도 포함되어야 합니다. 실제 개발에서는 다중 및 다중 유형 비동기 I/O 호출 시나리오가 종종 포함됩니다. 이러한 비동기 I/O 콜백 호출을 합리적으로 정렬하고 비동기 콜백의 질서 있는 진행을 보장하는 방법은 또한 어려운 문제입니다. 비동기 I/O /O 외에도 타이머와 같은 비I/O 비동기 호출도 있습니다. 이러한 API는 실시간이며 그에 따라 우선순위가 더 높은 콜백을 예약하는 방법은 무엇입니까? 🎜🎜따라서 이러한 작업이 메인 스레드에서 순서대로 실행되도록 하려면 다양한 우선 순위와 유형의 비동기 작업을 조정하는 예약 메커니즘이 있어야 합니다. 브라우저와 마찬가지로 Node는 이러한 무거운 작업을 수행하기 위해 이벤트 루프를 선택했습니다. 🎜🎜노드는 유형과 우선순위에 따라 작업을 타이머, 보류 중, 유휴, 준비, 폴링, 확인, 닫기 등 7가지 범주로 나눕니다. 각 작업 유형마다 작업과 해당 콜백을 저장하는 선입 선출 작업 대기열이 있습니다(타이머는 작은 상단 힙에 저장됨). 이 7가지 유형을 기반으로 Node는 이벤트 루프의 실행을 다음 7가지 단계로 나눕니다. 🎜

타이머🎜🎜이 단계의 실행 우선순위가 가장 높습니다. 🎜🎜이 단계에서 이벤트 루프는 타이머를 저장하는 데이터 구조(최소 힙)를 확인하고 타이머를 순회하며 현재 시간과 만료 시간을 하나씩 비교하고 타이머가 만료되었는지 확인합니다. 만료되면 타이머는 타이머 콜백 함수를 꺼내서 실행합니다. 🎜

pending🎜🎜이 단계에서는 네트워크, IO 및 기타 예외가 발생할 때 콜백을 실행합니다. *nix에 의해 보고된 일부 오류는 이 단계에서 처리됩니다. 또한 이전 주기의 폴 단계에서 실행되어야 하는 일부 I/O 콜백이 이 단계로 연기됩니다. 🎜🎜idle and prepare🎜🎜이 두 단계는 이벤트 루프 내에서만 사용됩니다. 🎜

poll

检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调、定时器调度的回调和 之外几乎所有回调setImmediate());节点会在适当的时候阻塞在这里。

poll,即轮询阶段是事件循环最重要的阶段,网络 I/O、文件 I/O 的回调都主要在这个阶段被处理。该阶段有两个主要功能:

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

  • 处理 I/O 队列中的回调。

当事件循环进入 poll 阶段并且没有设置定时器时:

  • 如果轮询队列不为空,则事件循环将遍历该队列,同步地执行它们,直到队列为空或达到可执行的最大数量。

  • 如果轮询队列为空,则会发生另外两种情况之一:

    • 如果有 setImmediate() 回调需要执行,则立即结束 poll 阶段,并进入 check 阶段以执行回调。

    • 如果没有 setImmediate() 回调需要执行,事件循环将停留在该阶段以等待回调被添加到队列中,然后立即执行它们。在超时时间到达前,事件循环会一直停留等待。之所以选择停留在这里是因为 Node 主要是处理 IO 的,这样可以更及时地响应 IO。

一旦轮询队列为空,事件循环将检查已达到时间阈值的定时器。如果有一个或多个定时器达到时间阈值,事件循环将回到 timers 阶段以执行这些定时器的回调。

check

该阶段会依次执行 setImmediate() 的回调。

close

该阶段会执行一些关闭资源的回调,如 socket.on('close', ...)。该阶段晚点执行也影响不大,优先级最低。

当 Node 进程启动时,它会初始化事件循环,执行用户的输入代码,进行相应异步 API 的调用、计时器的调度等等,然后开始进入事件循环:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<p>事件循环的每一轮循环(通常被称为 tick),会按照如上给定的优先级顺序进入七个阶段的执行,每个阶段会执行一定数量的队列中的回调,之所以只执行一定数量而不全部执行完,是为了防止当前阶段执行时间过长,避免下一个阶段得不到执行。</p><p>OK,以上就是事件循环的基本执行流程。现在让我们来看另外一个问题。</p><p>对于以下这个场景:</p><pre class="brush:php;toolbar:false">const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});
로그인 후 복사

当服务成功绑定到 8000 端口,即 listen() 成功调用时,此时 listening 事件的回调还没有绑定,因此端口成功绑定后,我们所传入的 listening 事件的回调并不会执行。

再思考另外一个问题,我们在开发中可能会有一些需求,如处理错误、清理不需要的资源等等优先级不是那么高的任务,如果以同步的方式执行这些逻辑,就会影响当前任务的执行效率;如果以异步的方式,比如以回调的形式传入 setImmediate() 又无法保证它们的执行时机,实时性不高。那么要如何处理这些逻辑呢?

基于这几个问题,Node 参考了浏览器,也实现了一套微任务的机制。在 Node 中,除了调用 new Promise().then() 所传入的回调函数会被封装成微任务外,process.nextTick() 的回调也会被封装成微任务,并且后者的执行优先级比前者高。

有了微任务后,事件循环的执行流程又是怎么样的呢?换句话说,微任务的执行时机在什么时候?

  • 在 node 11 及 11 之后的版本,一旦执行完一个阶段里的一个任务就立刻执行微任务队列,清空该队列。

  • 在 node11 之前执行完一个阶段后才开始执行微任务。

因此,有了微任务后,事件循环的每一轮循环,会先执行 timers 阶段的一个任务,然后按照先后顺序清空 process.nextTick()new Promise().then() 的微任务队列,接着继续执行 timers 阶段的下一个任务或者下一个阶段,即 pending 阶段的一个任务,按照这样的顺序以此类推。

利用 process.nextTick(),Node 就可以解决上面的端口绑定问题:在 listen() 方法内部,listening 事件的发出会被封装成回调传入 process.nextTick() 中,如下伪代码所示:

function listen() {
    // 进行监听端口的操作
    ...
    // 将 `listening` 事件的发出封装成回调传入 `process.nextTick()` 中
    process.nextTick(() => {
        emit('listening');
    });
};
로그인 후 복사

在当前代码执行完毕后便会开始执行微任务,从而发出 listening 事件,触发该事件回调的调用。

一些注意事项

由于异步本身的不可预知性和复杂性,在使用 Node 提供的异步 API 的过程中,尽管我们已经掌握了事件循环的执行原理,但是仍可能会有一些不符合直觉或预期的现象产生。

比如定时器(setTimeoutsetImmediate)的执行顺序会因为调用它们的上下文而有所不同。如果两者都是从顶层上下文中调用的,那么它们的执行时间取决于进程或机器的性能。

我们来看以下这个例子:

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

setImmediate(() => {
  console.log('immediate');
});
로그인 후 복사

以上代码的执行结果是什么呢?按照我们刚才对事件循环的描述,你可能会有这样的答案:由于 timers 阶段会比 check 阶段先执行,因此 setTimeout() 的回调会先执行,然后再执行 setImmediate() 的回调。

实际上,这段代码的输出结果是不确定的,可能先输出 timeout,也可能先输出 immediate。这是因为这两个定时器都是在全局上下文中调用的,当事件循环开始运行并执行到 timers 阶段时,当前时间可能大于 1 ms,也可能不足 1 ms,具体取决于机器的执行性能,因此 setTimeout() 在第一个 timers 阶段是否会被执行实际上是不确定的,因此才会出现不同的输出结果。

(当 delaysetTimeout 的第二个参数)的值大于 2147483647 或小于 1 时, delay 会被设置为 1。)

我们接着看下面这段代码:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
로그인 후 복사

可以看到,在这段代码中两个定时器都被封装成回调函数传入 readFile 中,很明显当该回调被调用时当前时间肯定大于 1 ms 了,所以 setTimeout 的回调会比 setImmediate 的回调先得到调用,因此打印结果为:timeout immediate

以上是在使用 Node 时需要注意的与定时器相关的事项。除此之外,还需注意 process.nextTick()new Promise().then() 还有 setImmediate() 的执行顺序,由于这部分比较简单,前面已经提到过,就不再赘述了。

总结

文章开篇从为什么要异步、如何实现异步两个角度出发,较详细地阐述了 Node 事件循环的实现原理,并提到一些需要注意的相关事项,希望对你有所帮助。

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

위 내용은 Node의 비동기 및 이벤트 루프의 기본 구현 및 실행 메커니즘에 대해 자세히 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

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