2021. 1. 3. 18:57ㆍ개발/Node & Javascript
개요
node의 event loop에 대해서는 개략적으로만 알고 있었는데, 이번 기회에 한 번 정리해보려고 한다.
구조
우선 해당 블로그에 내용이 잘 정리되어 있어 내용은 링크로 대체한다.
https://sjh836.tistory.com/149?category=710138
Microtask queue, task queue
기본적으로 Node.js는 event queue를 통해서 task queue를 관리를 하고 있는데,
단일 event queue로 관리되는 것이 아니라 각각의 역할에 따라서 queue가 존재한다.
가장 크게는 microstask queue와 task queue로 나눌 수 있다.
task queue는 기본적으로 application의 로직들의 처리를 담당하고 있고,
microtask queue들은 비동기 로직( Promise, setTimeout등 )을 제어하는 형태라고 볼 수 있다.
이 microtask queue는 또 두 가지 타입의 특별한 microtask queue들이 존재하는데,
- process.nextTick()을 처리하는 microtask queue가 있고,
- promise를 handle하는 microtask queue가 존재한다.
해당 microtask queue에 있는 callback들은 phase에 존재하는 normal queue보다 priority가 높고, nextTick의 microtask queue는 promise를 처리하는 task보다 항상 먼저 실행된다.
Event loop lifecycle
Event loop는 다음과 같은 처리 순서를 가진다.
각각의 phase별로 queue를 가지고 있다. 해당 스텝이 이벤트 루프에서 주기적으로 실행된다고 생각하면 된다.
출처: https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
1. poll
먼저 poll 단계이다.
- I/O 블로킹 또는 폴링해야되는지 계산한다.
- poll 큐에 있는 이벤트들을 처리한다.
poll 단계에서는 queue의 상태에 따라 동작이 다른데, 순서는 아래와 같다.
- queue가 비어있지 않다면: 이벤트 루프가 콜백의 큐를 순회하면서 큐를 다 소진하거나 시스템 의존적인 하드 한계에 도달할 때까지 동기로 콜백을 실행
- queue가 비어있다면:
- 스크립트가 setImmediate()로 스케줄링되었다면 이벤트 루프는 poll 단계를 종료하고 스케줄링된 스크립트를 실행하기 위해 check 단계 수행
- setImmediate()로 스케줄링되지 않았다면 이벤트 루프는 콜백이 큐에 추가되기를 기다린 후 즉시 실행
- 그리고 추가적으로 timer가 시간 임계점에 도달했는지 체크하고, 하나 이상의 타이머가 준비되었다면, timers 단계로 돌입한다.
2. check
callback 로직을 실행하는 단계이다.
3. close
소켓이나 핸들이 갑자기 닫힌 경우(예: socket.destroy()) 이 단계에서 'close' 이벤트가 발생한다.
그렇지 않으면 process.nextTick()으로 실행된다.
4. timers
setTimeOut에 대한 callback이 등록되고, 실행된다. 이 때 주의할 점은
timeout으로 등록된 timer의 값에 딱 맞게 실행되지 않을 수 있다.
왜냐하면 poll => callback 태스크를 먼저 처리하고, poll이 비어있을 경우에만 timers 단계로 돌입하게 되기 때문에 만약 poll에 태스크가 밀려있다면 시간은 부정확해질 수 있다.
5. pending callback
TCP 오류 같은 시스템 작업의 콜백을 실행한다.
예를 들어 ECONNREFUSED같은 에러를 받게 될 경우 해당 큐에 추가가 되게 된다.
process.nextTick()
nextTick 함수는 특수한 형태로 실행되기 때문에 따로 챕터를 마련하였다.
위에서 말한대로 nextTick은 자체적인 microtask queue를 가지고 있고,
각 단계에서 일반 task queue에 있는 callback보다 우선 실행된다.
다음의 두 가지 코드를 한 번 보자.
// case 1.
const nextTickRecursion = () => process.nextTick(nextTickRecursion);
nextTickRecursion();
setInterval(() => console.log('Hello, world'), 100);
// case 2.
const setImmediateRecursion = () => setImmediate(setImmediateRecursion);
setImmediateRecursion();
setInterval(()=> console.log('Hello, world'), 100);
위의 case는 nextTick을 통해 호출하는 예제이고, 아래는 setImmediate를 통해 호출하는 예제이다.
위의 case에서는 예상하시다시피 Hello world가 안 찍히는 것을 확인할 수 있다.
왜냐하면 nextTick은 항상 우선적으로 처리되기 때문에, 해당 callback만 계속 수행이 되기 때문이다.
그렇다면 사이드 이펙트가 많아보이는데, 굳이 nextTick을 쓰는 이유는 무엇일까?
node.js 공식 가이드의 설명에서는 nextTick을 사용하는 이유에 대해서 아래와 같이 설명하고 있다.
1. 사용자가 이벤트 루프를 계속하기 전에 오류를 처리하고 불필요한 자원을 정리하고 요청을 다시 시도할 수 있게 합니다.
2. 호출 스택은 풀린 뒤에도 이벤트 루프를 계속 진행하기 전에 콜백을 실행해야 하는 경우가 있습니다.
위의 설명과 관련하여 아래의 예제코드가 존재하는데,
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
사용자는 connection => listening이 실행될 것을 기대할 것이다.
이 때 connection이 우선적으로 실행되는 것이 보장하기 위해서 nextTick을 활용할 수 있다.
실험
그렇다면 다음의 코드의 실행순서를 예측함으로써 내용을 복습해보자.
결과는 직접 돌려보는 걸 추천하기 때문에, 따로 기재하지 않겠다.
// 출처: https://github.com/tlhunter/distributed-node/blob/master/event-loop-phases.js
const fs = require('fs');
Promise.resolve().then(() => console.log(2));
setImmediate(() => console.log(1));
process.nextTick(() => console.log(3));
fs.readFile(__filename, () => {
console.log(4);
setTimeout(() => console.log(5));
setImmediate(() => console.log(6));
process.nextTick(() => console.log(7));
});
console.log(8);
출처
- https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- https://nodejs.org/ko/docs/guides/
- Distributed Systems with Node.js
- https://www.quora.com/What-is-the-architecture-behind-Node-js
- https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/
- https://sjh836.tistory.com/149?category=710138
'개발 > Node & Javascript' 카테고리의 다른 글
Nest.js 탐험기3 - cache를 써보자 (3) | 2021.01.20 |
---|---|
Nest.js 탐험 2 - Filter를 등록해보자. (1) | 2021.01.16 |
ECMA 2020 Changes (3) | 2020.12.17 |
Nest.js 탐험 2 - API 작성해보기 (4) | 2020.12.13 |
Nest.js 탐험 1 - 튜토리얼 맛보기 (1) | 2020.11.28 |