Blog

이벤트 루프

개요

JavaScript를 사용하다 보면 Single-ThreadedEvent Loop 에 대한 내용을 자주 접하게 된다.
여러 글을 읽다가 한번 정리해야겠다는 생각이 들어서 정리해보려고 한다.

시작하기 전에

아래 코드를 실행하면 결과가 어떻게 출력될 지 생각해보자.
console.log('Main 1'); const interval = setInterval(() => { console.log('interval 1'); }, 0); setTimeout(() => { console.log('timeout 1'); Promise.resolve() .then(() => { console.log('promise 1'); }) .then(() => { console.log('promise 2'); clearInterval(interval); }); console.log('timeout 2'); }) console.log('Main 2'); Promise.resolve() .then(() => { console.log('promise 3'); }) .then(() => { console.log('promise 4'); });
JavaScript
복사

JavaScript Engine

자바스크립트 엔진은 Memory HeapCall Stack 으로 구성되어 있다.
가장 유명한 것이 Google V8 Engine 이다.
JavaScript는 Single-Threaded 이다. ( Call Stack을 1개만 사용한다 )
싱글 스레드는 한 번에 한 개의 작업만 실행할 수 있다.
이러한 특징 때문에 자바스크립트의 함수가 동작하는 방식을 Run to Completion이라고 한다.
일반적으로는 한 번에 한 작업만 실행할 수 있는 게 큰 문제가 되지 않지만, 한 개의 작업이 오래 걸리면 뒤에 있는 다른 작업이 실행이 안 되고 계속 지연된다.
Memory Heap: 동적으로 생성된 객체 ( 인스턴스 ) 가 저장되는 영역
Call Stack: 코드가 실행될 때 쌓이는 영역 ( Stack 형태 )

Task Queue

Event Queue == Task Queue == Message Queue ( 모두 같은 말 )
NodeJS 는 2개의 Queue( Macro Task, Micro Task )를 사용한다.
Macro Task를 1개 수행한 후 Micro Task Queue에 있는 모든 Task를 수행한다. ( 혹은 지정된 Max Count까지 수행한다. )
Macro Task Queue
I/O
setInterval
setImmediate
setTimeout
Micro Task Queue
process.nextTick
Promise
Object.observe

Event Loop

Node.js 는 libuv 를 사용한다.
이벤트 루프는 가능하면 언제나 시스템 커널에 Task를 넘겨서 Node.js 가 Non Blocking I/O 작업을 수행하도록 한다.
대부분의 커널은 Multi-Thread이기 때문에 백그라운드에서 여러 개의 Task를 수행할 수 있다. 이러한 작업 중 하나가 완료되면 커널이 Node.js 에게 알려줘서 Callback을 Poll Queue에 추가할 수 있도록 한다.
Event Queue는 총 7개가 있으며, 각 Queue를 Phase ( 단계 )라고 표현한다.
이벤트 루프가 실행하는 사이 Node.js 는 다른 비동기 I/O 나 Timer를 기다리고 있는지 확인하고, 없으면 종료한다.
각 Phase 들은 Callback의 FIFO Queue를 가지고 있다.

Timers

setTimeout, setInterval Callback 이 실행되는 단계
Timer Phase에 Task를 넣는 것은 Poll Phase에서 실행
Timer Phase는 단지 시간 관련 함수를 실행

Pending Callbacks

Loop의 반복으로 연기된 I/O Callback 들을 실행

idle/prepare

내부용으로만 사용

Poll

I/O 를 얼마나 오래 Block 해야 하고 Polling 해야 하는지 계산함
Poll Queue에 있는 이벤트를 처리함
Scheduling 된 타이머가 없으면
1.
Poll Queue가 비어있지 않다면 이벤트 루프가 Queue를 처리하면서 다 처리하거나 최대치에 도달할 때까지 동기적으로 Callback을 실행함
2.
Poll Queue가 비어있다면
setImmediate 가 스케줄링 되어 있다면 Poll Phase를 종료하고 Check Phase 로 넘어감
setImmediate 가 스케줄링 되어 있지 않으면 Callback이 Queue에 추가될 때까지 기다린 후 즉시 실행함

Check

setImmediate Callback 이 실행되는 단계
Poll 실행 직후 바로 실행됨

Close

일부 Close Callback 들이 실행되는 단계
socket.close

연습 문제

setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
JavaScript
복사
출력 결과는?
1.
timeoutimmediate
2.
immediatetimeout
3.
알 수 없다.
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
JavaScript
복사
출력 결과는?
1.
timeoutimmediate
2.
immediatetimeout
3.
알 수 없다.

레퍼런스