>웹 프론트엔드 >JS 튜토리얼 >Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명

Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명

不言
不言원래의
2018-08-15 14:26:472318검색

이 글은 Node.js의 이벤트 루프 워크플로우와 라이프사이클에 대한 자세한 설명을 제공합니다. 필요한 친구들이 참고할 수 있기를 바랍니다.

이 글에서는 node.js 이벤트 루프 워크플로와 수명 주기를 자세히 설명합니다.

몇 가지 일반적인 오해

js 엔진 내부의 이벤트 루프

가장 일반적인 오해 중 하나는 이벤트 루프가 다음과 같다는 것입니다. Javascript 엔진(V8, spiderMonkey 등)의 일부입니다. 실제로 이벤트 루프는 주로 Javascript 엔진을 사용하여 코드를 실행합니다.

스택 또는 큐가 있습니다

우선 스택이 없습니다. 둘째, 이 프로세스는 여러 큐(데이터 구조의 큐와 같은)가 포함되어 복잡합니다. 그러나 대부분의 개발자는 단일 대기열에 얼마나 많은 콜백 함수가 푸시되는지 알고 있는데 이는 완전히 잘못된 것입니다.

이벤트 루프는 별도의 스레드에서 실행됩니다.

잘못된 node.js 이벤트 루프 다이어그램으로 인해 우리 중 일부는 스레드가 두 개 있다고 생각했습니다. 하나는 Javascript를 실행하고 다른 하나는 이벤트 루프를 실행합니다. 실제로 이들은 모두 하나의 스레드에서 실행됩니다.

setTimeout에는 비동기 OS 시스템이 관여합니다

또 다른 매우 큰 오해는 setTimeout의 콜백 함수가 주어진 지연이 완료된 후 (아마도 OS 또는 커널에 의해) 대기열로 푸시된다는 것입니다.

setImmediate는 콜백 함수를 먼저 배치합니다.

일반적인 이벤트 루프 설명에는 대기열이 하나만 있으므로 일부 개발자는 setImmediate가 콜백을 작업 대기열의 맨 앞에 배치한다고 생각합니다. 이것은 완전히 잘못된 것입니다. Javascript의 작업 대기열은 선입선출입니다

이벤트 루프의 아키텍처

이벤트 루프의 워크플로를 설명하기 시작할 때 해당 아키텍처를 아는 것이 매우 중요합니다. 다음 그림은 이벤트 루프의 실제 작업 흐름을 보여줍니다.

Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명

그림의 다양한 상자는 다양한 단계를 나타내며 각 단계는 특정 작업을 수행합니다. 각 단계에는 대기열이 있으며(여기서는 주로 더 나은 이해를 위해 대기열이라고 합니다. 실제 데이터 구조는 대기열이 아닐 수 있습니다) Javascript는 모든 단계(유휴 및 준비 제외)에서 실행될 수 있습니다. 그림에서 nextTickQueue 및 microTaskQueue도 볼 수 있습니다. 이들은 루프의 일부가 아니며 포함된 콜백은 모든 단계에서 실행될 수 있습니다. 실행 우선순위가 더 높습니다.

이제 이벤트 루프가 다양한 단계와 다양한 대기열의 조합이라는 것을 아셨습니다. 아래에는 각 단계에 대한 설명이 나와 있습니다.

타이머 단계

이 단계에 바인딩된 큐는 타이머의 콜백(setTimeout, setInterval)을 유지하지만 콜백을 큐에 푸시하지는 않습니다. 그러나 가장 작은 힙을 사용하여 타이머를 유지하고 지정된 이벤트에 도달한 후 콜백을 실행합니다.

보류 중인 I/O 콜백 단계

이 단계에서는 이벤트 루프의 보류 중인_큐에 있는 콜백을 실행합니다. 이러한 콜백은 이전 작업에 의해 푸시됩니다. 예를 들어 tcp에 무언가를 쓰려고 하면 작업이 완료되고 콜백이 대기열에 푸시됩니다. 오류 처리를 위한 콜백도 여기에 있습니다.

Idle, Preparation 단계

이름은 Idle이지만 매 틱마다 실행됩니다. 폴링 단계가 시작되기 전에 준비도 실행됩니다. 어쨌든 이 두 단계는 노드가 주로 일부 내부 작업을 수행하는 단계입니다.

Poll 단계

전체 이벤트 루프에서 가장 중요한 단계는 아마도 Poll 단계일 것입니다. 이 단계에서는 새로 들어오는 연결(새 소켓 설정 등)과 데이터(파일 읽기 등)를 허용합니다. 폴링 단계를 여러 부분으로 나눌 수 있습니다.

  1. watch_queue(폴링 단계에 바인딩된 대기열)에 항목이 있으면 대기열이 비어 있거나 시스템이 최대 한도에 도달할 때까지 차례로 실행됩니다.

  2. 큐가 비어 있으면 노드는 새로운 연결을 기다립니다. 대기 또는 절전 이벤트는 다양한 요인에 따라 달라집니다.

Check 단계

폴링의 다음 단계는 setImmediate 전용인 check pahse입니다. setImmediate 콜백을 처리하기 위해 전용 대기열이 필요한 이유는 무엇입니까? 이는 폴링 단계의 동작 때문이며 나중에 프로세스 섹션에서 설명하겠습니다. 지금은 확인 단계에서 주로 setImmediate() 콜백을 처리한다는 점만 기억하세요.

콜백 닫기

콜백 닫기(stocket.on('close', () => {}))는 모두 정리 단계처럼 여기에서 처리됩니다.

nextTickQueue 및 microTaskQueue

nextTickQueue의 작업은 process.nextTick()에 의해 트리거된 콜백에 유지됩니다. microTaskQueue는 Promise에 의해 트리거된 콜백을 유지합니다. 그 중 어느 것도 이벤트 루프(libUV에서 개발되지 않음)의 일부가 아니지만 node.js에 있습니다. C/C++와 Javascript가 교차할 때 최대한 빠르게 호출됩니다. 따라서 현재 작업이 실행된 후에 실행되어야 합니다(반드시 현재 js 콜백이 실행된 이후는 아님).

이벤트 루프 워크플로

콘솔에서 node my-script.js를 실행할 때 노드는 이벤트 루프를 설정한 다음 이벤트 루프 외부에서 메인 모듈(my-script.js)을 실행합니다. 메인 모듈이 실행되면 노드는 루프가 아직 살아 있는지 확인합니다(이벤트 루프에서 할 일이 남아 있습니까?). 그렇지 않은 경우 종료 콜백을 실행한 후 종료됩니다. 프로세스, on('exit', foo) 콜백(종료 콜백). 그러나 루프가 아직 살아 있으면 노드는 타이머 단계에서 루프에 들어갑니다.

Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명

Timer 단계의 작업 흐름

이벤트 루프는 타이머 단계에 들어가 타이머 큐에 실행해야 할 항목이 있는지 확인합니다. 아주 간단해 보이지만 실제로 이벤트 루프는 적절한 콜백을 찾기 위해 몇 가지 단계를 수행해야 합니다. 실제로 타이머 스크립트는 오름차순으로 힙 메모리에 저장됩니다. 먼저 실행 타이머를 획득하고 now-registeredTime == delta?인지 여부를 계산합니다. 그렇다면 타이머의 콜백을 실행하고 다음 타이머를 확인합니다. 아직 예약되지 않은 타이머를 찾을 때까지 다른 타이머 확인을 중지하고(타이머가 오름차순으로 정렬되어 있으므로) 다음 단계로 이동합니다.
setTimeout을 4번 호출하여 시간 t에 대해 100, 200, 300, 400의 차이가 있는 4개의 타이머를 생성한다고 가정합니다.

Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명

이벤트 루프가 t+250에서 타이머 단계에 진입한다고 가정합니다. 먼저 만료 시간이 t+100인 타이머 A를 살펴보겠습니다. 그러나 지금은 t+250이다. 따라서 타이머 A에 바인딩된 콜백을 실행합니다. 그런 다음 타이머 B를 확인하고 만료 시간이 t+200이므로 B의 콜백도 실행된다는 것을 확인합니다. 이제 C를 확인하고 만료 시간이 t+300임을 확인하므로 그대로 둡니다. 타이머가 오름차순으로 설정되어 D가 C보다 더 큰 임계값을 갖기 때문에 타임 루프는 D를 확인하지 않습니다. 그러나 이 단계에는 시스템 종속적인 하드 제한이 있습니다. 시스템 종속성의 최대 수에 도달하면 실행되지 않은 타이머가 있어도 다음 단계로 이동합니다.

보류 중인 I/O 단계 워크플로

타이머 단계 후에 이벤트 루프는 보류 중인 I/O 단계에 들어간 다음 이전 보류 중인_queue 콜백에서 보류 중인 작업이 있는지 확인합니다. 있는 경우 대기열이 비어 있거나 시스템의 최대 한계에 도달할 때까지 차례로 실행합니다. 그 후, 이벤트 루프는 유휴 핸들러 단계로 이동하고, 이어서 일부 내부 작업을 수행하기 위한 준비 단계가 진행됩니다. 그러면 마침내 가장 중요한 단계인 설문 조사 단계에 들어갈 수 있습니다.

Poll 단계 작업 흐름

이름에서 알 수 있듯이 이것은 관찰 단계입니다. 새로운 요청이나 연결이 들어오는지 관찰하세요. 이벤트 루프가 폴링 단계에 들어가면 이벤트가 소진되거나 다른 단계처럼 시스템 종속성 제한에 도달할 때까지 파일 읽기 응답, 새 소켓 또는 http 연결 요청을 포함하여 watcher_queue의 스크립트를 실행합니다. 실행할 콜백이 없다고 가정하면 폴링은 특정 조건에서 잠시 대기합니다. 확인 대기열, 보류 대기열, 닫기 콜백 대기열 또는 유휴 핸들러 대기열에 대기 중인 작업이 있는 경우 0밀리초 동안 대기합니다. 그런 다음 타이머 힙을 기반으로 첫 번째 타이머(사용 가능한 경우)를 실행하기 위한 대기 시간을 결정합니다. 첫 번째 타이머 임계값이 전달되면 기다릴 필요가 없습니다(첫 번째 타이머가 실행됩니다).

확인 단계 워크플로

폴링 단계가 끝나면 즉시 확인 단계가 시작됩니다. 이 단계의 대기열에는 api setImmediate에 의해 트리거되는 콜백이 있습니다. 대기열이 비어 있거나 종속 시스템의 최대 한도에 도달할 때까지 다른 단계와 마찬가지로 하나씩 실행됩니다.

닫기 콜백 작업 흐름

확인 단계의 작업을 완료한 후 이벤트 루프의 다음 대상은 닫기 또는 파괴 유형의 닫기 콜백을 처리하는 것입니다. 이벤트 루프는 이 단계에서 큐에 있는 콜백 실행을 마친 후 루프가 아직 살아 있는지 확인하고 그렇지 않으면 종료됩니다. 그러나 아직 수행할 작업이 있으면 다음 루프로 이동하므로 타이머 단계가 됩니다. 이전 예의 타이머(A & B)가 만료된 것으로 간주하면 이제 타이머 단계는 타이머 C에서 시작되어 만료를 확인합니다.

nextTickQueue & microTaskQueue

그렇다면 이 두 대기열의 콜백 기능은 언제 실행되나요? 물론 현재 단계에서 다음 단계로 넘어가기 전에 최대한 빠르게 실행됩니다. 다른 단계와 달리 시스템에 따른 숙취 제한이 없으며 두 대기열이 모두 빌 때까지 노드가 실행됩니다. 그러나 nextTickQueue는 microTaskQueue보다 작업 우선순위가 더 높습니다.

프로세스 풀(스레드 풀)

Javascript 개발자들로부터 자주 듣는 단어는 ThreadPool입니다. 일반적인 오해는 nodejs에 모든 비동기 작업을 처리하는 프로세스 풀이 있다는 것입니다. 그러나 실제로 프로세스 풀은 libUV(비동기 처리를 위해 nodejs에서 사용하는 타사 라이브러리) 라이브러리에 있습니다. 다이어그램에 표시되지 않은 이유는 사이클 메커니즘의 일부가 아니기 때문입니다. 현재 모든 비동기 작업이 프로세스 풀에서 처리되는 것은 아닙니다. libUV는 운영 체제의 비동기 API를 유연하게 사용하여 환경을 이벤트 중심으로 유지합니다. 그러나 운영 체제의 API는 파일 읽기, DNS 쿼리 등을 수행할 수 없습니다. 이러한 작업은 기본적으로 4개의 프로세스만 있는 프로세스 풀에서 처리됩니다. uv_threadpool_size의 환경 변수를 128까지 설정하여 프로세스 수를 늘릴 수 있습니다.

예제가 포함된 워크플로

이벤트 루프가 어떻게 작동하는지 이해할 수 있기를 바랍니다. C 언어에서 동기식은 Javascript가 비동기식으로 변하는 데 도움이 됩니다. 한 번에 한 가지만 처리하지만 매우 차단적입니다. 물론 이론을 설명할 때마다 예제를 통해 가장 잘 이해되므로 몇 가지 코드 조각을 통해 이 스크립트를 이해해 보겠습니다.

클립 1—기본 이해
setTimeout(() => {console.log('setTimeout'); }, 0); 
setImmediate(() => {console.log('setImmediate'); });

위의 결과를 추측할 수 있나요? 글쎄요, setTimeout이 먼저 인쇄될 것이라고 생각할 수도 있지만 보장할 수는 없습니다. 왜 그렇습니까? 메인 모듈을 실행하고 타이머 단계에 들어간 후에도 타이머가 만료된 것을 발견하지 못했거나 발견하지 못했을 수도 있습니다. 왜? 타이머 스크립트는 사용자가 제공한 시스템 시간과 델타 시간을 기반으로 등록됩니다. setTimeout이 호출됨과 동시에 타이머 스크립트가 메모리에 기록되며 머신의 성능과 머신에서 실행 중인 기타 작업(노드 아님)에 따라 약간의 지연이 있을 수 있습니다. 다른 지점에서 노드는 타이머 단계(각 순회 라운드)에 들어가기 전에 지금을 현재 시간으로 사용하여 변수를 설정합니다. 따라서 정확한 시간과 동등한 것에 문제가 있다고 말할 수 있습니다. 이것이 불확실성의 이유입니다. 타이머 코드의 콜백에서 동일한 코드를 가리키면 동일한 결과를 얻게 됩니다.

그러나 이 코드를 I/O 주기로 이동하면 setImmediate 콜백이 setTimeout 전에 실행된다는 것이 보장됩니다.

fs.readFile('my-file-path.txt', () => {
    setTimeout(() => {console.log('setTimeout');}, 0);               
    setImmediate(() => {console.log('setImmediate');}); });
클립 2 — 타이머에 대한 더 나은 이해
var i = 0;
var start = new Date();
function foo () {
    i++;
    if (i <p>위의 예는 매우 간단합니다. foo 함수를 호출한 다음 1000까지 setImmediate를 통해 foo를 재귀적으로 호출합니다. 내 컴퓨터에서는 약 6~8밀리초 정도 걸렸습니다. 요정님, 위 코드를 수정하고 setImmedaite(foo)를 setTimeout(foo, o)로 바꾸세요. </p><pre class="brush:php;toolbar:false">var i = 0;
var start = new Date();
function foo () {
    i++;
    if (i <p>현재 내 컴퓨터에서 이 코드를 실행하는 데 1400ms 이상이 걸립니다. 왜 이런 일이 발생합니까? 이들 중 어느 것도 i/o 이벤트가 없으며 동일해야 합니다. 위 두 예의 대기 이벤트는 0입니다. 왜 이렇게 오래 걸리나요? 이벤트 비교를 통해 편차가 발견되었으며, CPU 집약적인 작업에는 더 많은 시간이 소요되었습니다. 등록된 타이머 스크립트도 이벤트를 소비합니다. 타이머의 각 단계에서는 타이머 실행 여부를 결정하기 위해 몇 가지 작업을 수행해야 합니다. 실행 시간이 길어지면 틱도 많아집니다. 그러나 setImmediate에는 마치 대기열에 있다가 실행되는 것처럼 확인하는 단계가 하나만 있습니다. </p><h5><strong>클립 3 — nextTick() 및 타이머 실행 이해</strong></h5><pre class="brush:php;toolbar:false">var i = 0;
function foo(){
    i++;
    if (i>20) return;
    console.log("foo");
    setTimeout(()=>console.log("setTimeout"), 0);       
    process.nextTick(foo);
}
setTimeout(foo, 2000);

위 출력이 무엇이라고 생각하시나요? 예, foo를 인쇄한 다음 setTimeout을 인쇄합니다. 2초 후에 foo()는 nextTickQueue에 의해 재귀적으로 호출되어 첫 번째 foo를 인쇄합니다. 모든 nextTickQueue가 실행되면 다른 것(예: setTimeout 콜백)도 실행됩니다.

그러면 각 콜백이 실행된 후 nextTickQueue가 확인되기 시작하나요? 코드를 변경해서 살펴보겠습니다.

var i = 0;
function foo(){
    i++;
    if (i>20) return;
    console.log("foo");
    setTimeout(()=>console.log("setTimeout"), 0);       
    process.nextTick(foo);
}
setTimeout(foo, 2000);
setTimeout(()=>{console.log("Other setTimeout"); }, 2000);

setTimeout 이후에 동일한 지연 시간으로 다른 setTimeout을 출력하는 또 다른 setTimeout을 추가했습니다. 보장되지는 않지만 첫 번째 foo가 출력된 후 기타 setTimeout이 출력될 가능성이 있습니다. 동일한 타이머를 그룹화하여 진행 중인 콜백 그룹이 실행된 후 nextTickQueue가 실행됩니다.

몇 가지 일반적인 질문

Javascript 코드는 어디에서 실행되나요?

대부분의 사람들이 이벤트 루프를 별도의 스레드에 있는 것으로 생각하고 콜백을 대기열에 넣은 다음 차례로 실행한다고 생각합니다. 이 글을 처음 읽는 독자라면 자바스크립트는 어디서 실행되는지 궁금할 것이다. 앞서 말했듯이 스레드는 하나 뿐이고 V8이나 다른 엔진을 사용하는 이벤트 루프 자체의 Javascript 코드도 여기에서 실행됩니다. 실행은 동기식이며 현재 Javascript 실행이 완료되지 않은 경우 이벤트 루프가 전파되지 않습니다.

setTimeout(fn, 0)이 있는데 왜 setImmediate가 필요한가요?

일단 0이 아니라 1입니다. 1보다 작거나 2147483647ms보다 큰 시간으로 타이머를 설정하면 자동으로 1로 설정됩니다. 따라서 setTimeout의 지연 시간을 로 설정하면 0이면 자동으로 1로 설정됩니다.

또한 setImmediate를 사용하면 추가 검사가 줄어듭니다. 따라서 setImmediate가 더 빠르게 실행됩니다. 또한 폴링 단계 뒤에 배치되므로 들어오는 요청의 setImmediate 콜백이 즉시 실행됩니다.

Comprehension에서 setImmediate를 호출하는 이유는 무엇인가요?

setImmediate 및 process.nextTick()의 이름이 모두 잘못되었습니다. 따라서 기능적으로 setImmediate는 다음 틱에서 실행되고 nextTick은 즉시 실행됩니다.

Javascript 코드가 차단되나요?

nextTickQueue에는 콜백 실행에 제한이 없기 때문입니다. 따라서 process.nextTick()을 재귀적으로 실행하면 다른 단계에 있는 내용에 관계없이 프로그램이 이벤트 루프에서 벗어날 수 없습니다.

exit 콜백 단계에서 setTimeout을 호출하면 어떻게 되나요?

타이머를 초기화할 수 있지만 콜백은 호출되지 않을 수 있습니다. 왜냐하면 노드가 종료 콜백 단계에 있다면 이미 이벤트 루프에서 빠져나왔기 때문입니다. 따라서 실행으로 돌아갈 수 없습니다.

몇 가지 짧은 결론

이벤트 루프에는 작업 스택이 없습니다

이벤트 루프는 별도의 스레드에 있지 않으며 Javascript의 실행은 실행을 위해 대기열에서 콜백을 꺼내는 것만큼 간단하지 않습니다.

setImmediate는 콜백을 작업 대기열의 헤드로 푸시하지 않으며 전용 스테이지와 대기열이 있습니다.

setImmediate는 다음 루프에서 실행되며, nextTick은 실제로 즉시 실행됩니다.

조심하세요. nextTickQueue가 재귀적으로 호출되면 노드 코드를 차단할 수 있습니다.

관련 권장 사항:

Node.js 이벤트 loop_node.js에 대한 심층 분석

JS Controls_javascript 기술의 수명 주기 소개

위 내용은 Node.js 이벤트 루프 워크플로 및 수명 주기에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.