본문으로 건너뛰기

이벤트 루프

개요​

JavaScript를 사용하다 보면 Single-Threaded 나 Event 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 Engine​

자바스크립트 엔진은 Memory Heap 과 Call 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를 가지고 있다.

각 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');
});

출력 결과는?

  1. timeout → immediate
  2. immediate → timeout
  3. 알 수 없다.
const fs = require('fs');

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

setImmediate(() => {
console.log('immediate');
});
});

출력 결과는?

  1. timeout → immediate
  2. immediate → timeout
  3. 알 수 없다.

레퍼런스​