본문 바로가기
Programming/Node.js

[Node.js] Event Loop와 non-blocking I/O 실행 과정

by SpiralMoon 2024. 7. 4.
반응형

Event Loop와 non-blocking I/O 실행 과정

Node.js가 Single Thread 임에도 어떻게 non-blocking I/O를 처리하는지 알아보자

사전 지식

스레드

 

스레드 (컴퓨팅) - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 두 개의 스레드를 실행하고 있는 하나의 프로세스. 스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으

ko.wikipedia.org

 

콜백

 

콜백 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 프로그래밍에서 콜백(callback) 또는 콜백 함수(callback function)는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을

ko.wikipedia.org


Node.js는 Single Thread이다

우선 node.js가 정말 Single Thread인지 알아보자.

 

// 1번 예시
setTimeout(() => {
    console.log('test');
}, 1000);

// 결과
test
test
test
...



// 2번 예시
setTimeout(() => {
    console.log('test');
}, 1000);

while (true) {

}

// 무한루프로 인한 blocking 상태 진입

 

위 소스코드는 node.js가 Single Thread 임을 증명할 수 있는 간단한 예시 코드다.

 

2번 예시를 실행했을 때 무한루프로 인한 blocking 상태에 진입한다. 그리고 1번 예시와 다르게 앞서 등록했던 callback 함수도 실행되지 않는다. callback 함수는 별도의 Thread가 아닌 기존의 Main Thread에서 동작한다는 것을 알 수 있다.

 

즉, node.js에서 개발자가 작성하는 자바스크립트 코드는 Single Thread로 동작한다.


Node.js의 주요 구성 요소

Node.js 아키텍처

node.js는 여러 라이브러리로 구성되어 있으며 I/O 동작을 이해하기 위해 알아야하는 것은 V8 Engine과 libuv이다.

 

V8 Engine (JS 실행 엔진)

  • heap memory 할당
  • call stack 실행
  • GC
  • bytecode compile

libuv (비동기 I/O 멀티플랫폼 라이브러리)

  • I/O 작업 실행
  • Thread Pool 보유
  • event loop를 통한 event queue 관리

libuv는 node.js가 Single Thread 임에도 멈추지 않고 실행되도록 I/O 작업을 non-blocking 방식으로 처리한다. 그리고 내부에 Thread Pool을 보유하고 있다.


libuv가 I/O 작업을 non-blocking 방식으로 처리하는 과정

V8 Engine과 libuv의 핵심 구성요소

 

libuv는 1개의 Event Loop여러개의 Worker Thread로 구성되어 있으며 I/O 작업을 non-blocking 방식으로 처리할 수 있다. node.js는 Single Thread인데 libuv는 Multi Thread로 실행될 수 있다는 뜻이다.

 

I/O 작업이 요청되면 해당 작업이 blocking, non-blocking 중에서 어느 방식이냐에 따라 다르게 처리한다.

 

non-blocking I/O를 실행하는 과정

커널의 비동기 I/O 지원을 받을 수 있는 I/O 작업인 경우 : 커널의 인터페이스를 통해 작업을 전달하고 실질적인 작업은 커널에서 비동기 방식으로 처리된다. 작업이 완료되면 해당 작업의 완료 이벤트를 libuv로 전달한다. libuv는 이러한 완료 이벤트를 받아서 Event Queue에 callback을 등록한다.

 

blocking I/O를 처리하는 과정

blocking I/O 작업인 경우 : libuv 내의 Thread Pool에서 Worker Thread를 선택하여 작업을 위임한다. Worker Thread는 작업을 완료하고 Event Queue에 callback을 등록한다. Main Thread와는 다른 Thread를 실행하기 때문에 병렬로 처리된다.

 

결국에는 모든 I/O 작업이 Main Thread가 멈추지 않는 구조로 실행된다.


Event Loop와 Event Queue

완료된 I/O 작업들은 앞에서 설명한 방법대로 callback의 형태로 Event Queue에 적재되었다. 이 callback들이 실행되기 위해서는 V8 Engine의 Call Stack으로 옮겨질 필요가 있다.

 

Event Loop가 callback을 옮기는 과정

 

Event Loop는 Main Thread에 의해 실행되며, Event Queue에 적재된 callback을 Call Stack이 비어있을 때 Call Stack으로 옮기는 작업을 지속적으로 수행하여 callback이 Main Thread에 의해 실행될 수 있도록 한다.

 

그런데 Event Queue는 1개가 아니다.

Event Loop와 Event Queue

 

libuv는 효율적인 비동기 처리를 위해 Queue를 6개로 세분화하였다. Event Loop는 이 6개의 Queue를 순차적으로 검사하면서 callback을 Call Stack으로 이동시킨다.

(여기서 callback을 queue에서 Call Stack으로 이동하는 하나의 작업을 tick이라고 한다.)

 

  • Timer : setTimeout(), setInterval()로 예약된 타이머 callback을 처리
  • Pending callbacks (I/O cycle) : 이전 Loop에서 완료된 callback 또는 error callback을 처리
  • Idle, Prepare (I/O cycle) : Event Loop의 초기화와 관련된 작업을 수행. Poll 단계를 준비
  • Poll (I/O cycle) : 새로운 I/O 이벤트의 커넥션과 데이터 처리 허용. 이 단계에서 Call Stack으로 제일 많이 이동함
  • Check : setImmediate()로 예약된 callback을 처리
  • Close callbacks : socket.on('close', () => {}) 처럼 자원 해제류의 callback을 처리

 

그리고 이러한 Queue들 사이에서 nextTickQueue와 microTaskQueue가 실행된다.

 

다음 코드를 통해서 callback의 실행 순서를 간단하게 확인할 수 있다.

 

// Check queue에서 실행됨
setImmediate(() => {
    console.log('Immediate');
});

// Timer queue에서 실행됨
setTimeout(() => {
    console.log('Timeout');
}, 0);

// 결과
Timeout
Immediate

 

Event Loop에서 Timer Queue가 Check Queue보다 먼저 처리되므로 setTimeout()의 callback이 먼저 실행되었다.


Node.js는 결국 무슨 구조인가

결론적으로 node.js는 Single Thread Event LoopMulti Thread libuv 라이브러리를 통한 비동기 I/O 처리를 결합한 복합적인 구조라고 할 수 있다.


참조

 

NodeJS Architecture & Concurrency Model

Single threaded JavaScript into asynchronous non-blocking I/O model using Event Loop

chathuranga94.medium.com

 

 

 
반응형

댓글