주로 Node.js 환경에서 개발하면서, 문득 이벤트 루프와 libuv에 대해 개념 정리가 하고싶어 공부 겸 글을 작성하게 되었다.
Node.js 환경에서 개발을 해본 개발자라면 이 내용들은 숙지를 하고 있을 것이다.
하지만 대부분이 알고있는 싱글 스레드는 하나의 작업이 끝낼 때 까지 기다려야해 동시에 여러 작업을 수행하지 못할 것으로 알려져 있다. 어떻게 Node.js는 하나의 스레드로 여러 비동기 작업들을 블로킹 없이 수행할 수 있는 것일까?
먼저 Node.js의 구성 요소를 파악해야 하는데, Node.js는 런타임 언어이고, 크게 내장 라이브러리, V8 엔진, 이벤트 루프(libuv)로 구성되어 있다.
이 구성 요소 중 Node.js가 하나의 스레드로 여러 작업을 블로킹 없이 수행할 수 있게 하는 핵심 요소는 libuv 라이브러리 덕분이다.
C언어 기반의 비동기 I/O 라이브러리.
libuv는 비동기 I/O 작업, 이벤트 처리, 동시성 및 기타 시스템 관련 기능에 대한 크로스 플랫폼 지원을 제공하는 라이브러리이다.
운영체제들의 커널(운영체제의 핵심부로 컴퓨터 자원들을 관리하는 역할)을 추상화해 다양한 운영 체제에서 일관되게 작동한다.
자바스크립트 엔진은 내부적으로 비동기 처리를 할 수 없다. 때문에 비동기로 처리되는 코드를 만날 경우 libuv 라이브러리를 통해 비동기 작업을 처리하게 된다.
libuv에 비동기 작업을 요청하면 libuv는 uv_io 를 통해 커널이 지원하는지 확인하고, 지원한다면 커널에 요청해 받은 응답을 전달한다.
libuv의 스레드 관련 내용은 더 아래에서 다뤄보겠다.
libuv는 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 따르고 있다. (이벤트 기반 비동기 처리)
만약 여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단해 실행하게 된다.
목록의 순서대로 호출되며, 각각의 단계는 자신만의 큐를 가지고 있다.
작업을 등록하면 유형에 맞게 각각 큐에 등록이 된다.
먼저 페이즈에서 페이즈로 넘어가는걸 Tick
이라고 정의한다.
루프의 시작을 알리는 페이즈.
주로 setTimeout
, setInterval
에 등록된 콜백을 관리한다. 여기서는 타이머에 등록된 콜백이 언제 실행될지만 관리하고 실행은 Poll 단계에서 실행된다.
Timer 콜백은 min-heap
자료구조 기반으로 작성되어있다.
min-heap
완전이진트리(complete binary tree)의 일종으로, 최솟값을 빠르게 찾아내기 위한 자료구조. 이를 통해 이벤트 루프가 가장 짧은 지연으로 타이머를 효율적으로 검색할 수 있도록 보장받는다.
다음 페이즈를 실행하기 위해 연기된 콜백을 실행하는 페이즈.
각 페이즈는 모든 작업을 실행하지 않고 일정량만 수행 후 다음 페이즈로 넘어가기 때문에 처리하지 못한 작업(pending_queue
)을 수행한다.
이 페이즈는 매 틱마다 수행된다. (Node.js 의 내부 관리를 위해 사용)
setTimeout
, setInterval
, setImmediate
로 등록한 콜백을 제외한 모든 콜백이 이 페이즈에서 수행된다.
해당 큐가 비어있지 않다면 작업을 순차적으로 처리하고, 비어있다면 다음 페이즈로 즉시 넘어가지 않고 일정시간 대기한다.
대기하는 시간은 Timer 페이즈에서 실행한 작업이 있는지 검증 후 넘어간다.
해당 페이즈가 검사하는 큐 이름은 watcher_queue
이며,
비동기 작업이 완료되었을 때 순서를 보장하기 위해 사용한다.
setImmediate
로 등록된 콜백을 관리하기 위한 페이즈이다.
socket.on('close', cb)
코드와 같이 close, destory 이벤트 타입 콜백을 처리하는 단계이다.
위에서 말했다싶이 libuv 에 파일 읽기와 같은 비동기 작업을 요청하면 uv_io
를 통해 커널이 지원하는지 확인한다.
만약 지원한다면 libuv가 uv_io
를 통해 커널에게 비동기적으로 요청하지만, 아니라면 자신만의 워커 스레드가 담긴 스레드 풀을 사용한다.
Thread Pool에서 처리되는 작업들은 다음과 같다.
file system : fs.FSWatcher()와 synchronous fs 제외
DNS : dns.lookup(), dns.lookupService()
Crypto : crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), crypto.randomFill(),
Zlib : synchronous API 제외
libuv는 기본적으로 4개의 스레드를 가지는 스레드풀을 생성하지만, 환경변수를 통해 스레드를 128개까지 늘릴 수 있다.
https://blog.naver.com/pjt3591oo/221976414901
https://www.korecmblog.com/blog/node-js-event-loop#nodejs-event-loop
https://sjh836.tistory.com/149
https://velog.io/@tastestar/Node.js-libuv