[Node.js_4기] Today_I_Learn : 이벤트 루프 정?리 (24/01/23)

2024. 1. 23. 19:57공부/내배캠 TIL

목차

 

1. 학습 내용

2. 내용 정리

3. 생각 정리

 

1. 학습 내용 

 

Node.js Event Loop에 대한 정리

싱글스레드 논블로킹I/O가 뭔지, 어떻게 작동하는지 내용을 정리

결과적으로, Node.js에대한 좀 더 깊은 이해를 위해 작성함.

Node.js 이벤트 루프(Event Loop) 샅샅이 분석하기 | 쿠키의 개발 블로그 (korecmblog.com) -> 캠프 자료 내부에 있었음

 

Node.js 이벤트 루프(Event Loop) 샅샅이 분석하기

Node.js의 이벤트 루프를 구현과 함께 자세히 살펴봅니다

www.korecmblog.com

이벤트 루프 기본 : 

호출 스택(Call Stack)과 이벤트 큐(Event Queue)를 관찰하면서,

//호출 스택이 비어있고, 이벤트 큐에 작업이 있다면\\ 이벤트 큐의 작업을 호출 스택으로 이동시키는 역할을 담당.

->

시간이 오래 걸리는 작업을 이벤트 큐에 넣어 비동기적으로 처리하고,

그 동안 호출 스택에서 다른 작업들을 계속 처리할 수 있습니다.

 

2. 내용 정리 

 

싱글 스레드 논 블로킹 

 

-> 하나의 스레드로 동작하기 때문에 빠르지만, 하나의 실행 흐름만을 가지고 있다.
-> 하지만, 이벤트 루프를 통해 비동기 작업들을 블로킹 없이 수행할 수 있다.

이것이 가능한 이유 -> Node.js내부에 존재하는 libuv 라이브러리 덕분.

 

libuv 

비동기 입출력, 이벤트 기반에 초점을 맞춘 라이브러리. (커널이 어떤 비동기 API를 지원하는지 알고 있음)
커널이 지원하는 비동기 작업을 libuv에 요청 -> 커널에게 이 작업을 비동기적으로 요청
커널이 지원하지 않는 비동기 작업을 요청 -> libuv내부의 스레드풀에 작업을 요청

&

Node.js는 I/O작업을 libuv에 위임한으로서 논 블로킹 I/O를 지원한다.

이 근간에 존재하는게 이벤트 루프라는 구현체.

 

이벤트 루프 

Node.js가 비동기 작업관리하기 위한 구현체. 모아서 관리하고, 순서를 보장해주는 도구라는 말.

= 페이즈 =

timer phase 
pending cllbakcs phase 
idle, prepare phase 
poll phase 
check phase 
close callbacks phase 

= 페이즈 전환 순서 =

timer phase -> pending cllbakcs phase -> idle, prepare phase -> poll phase -> check phase -> close callbacks phase -> timer phase  (페이즈에서 페이즈로 넘어가는 것을 tick이라 부른다)

각 페이즈는 큐(이벤트 루프가 실행해야 할 작업들이 순서대로 담긴 선입선출 자료구조)를 가지고 있고,

큐에에 담긴 모든 코드가 실행/시스템 실행 한도에 도달할 경우 다음 페이즈로 진입한다.

(비동기 실행을 도와주는 것과는 별개로, 한번에 하나의 페이즈에 진입하며 하나의 작업만 수행 가능하다 -> 싱글 스레드)

 

node.js는 페이즈에 진입해 큐에 쌓인 작업을 처리한다.

쌓인 작업 처리중에도 이전 페이즈에 실행한 작업의 콜백이나 커널이 정한 새로운 작업이 큐에 추가될 수 있고, 큐에 작업이 계속 추가되면 다음 페이즈로 넘어가지 못할 수 있다.(사실, 시스템 실행 한도 덕분에 페이즈에 갇히는 일은 없다.)

 

코드의 실행 전에 node.js는 이벤트 루프를 생성한다. ->

루프 밖에서 루프를 처음부터 끝까지 실행하여 이벤트 루프가 살아있을 때만(오류가 없을 때만) 진입한다. ->

exit callbacks를 실행한 후 프로그램을 종료 한다. ->

루프 진입시 페이즈를 차례대로 돌면서 실행할 수 있는 작업을 실행한다. ->

매 반복마다 루프의 생존을 확인한다. 루프가 죽으면, exit callbacks를 실행한 후 프로그램을 종료한다.

 

각 페이즈에 대한 정리

Timer Phase
setTimeout, setInterval같은 타이머들을 다루는 페이즈. (직접 콜백을 관리하진 않음)
min-heap을 이용해 실행 시간이 가장 이른 타이머를 효율적으로 찾을 수 있다.
(min-heap = 최솟값을 찾는데 최적화된 자료구조, 타이머를 오름차순으로 관리)
정확한 실행 시간을 보장하진 않는다. -> 1초후 실행되진 않는다.

대신, 시간이 흐르기 전에 실행되지 않는 것을 보장한다. -> 1초가 흐르기 전에는 실행되지 않는다.
모든 작업 실행/실행 한도에 다다르면 다음 페이즈로

// 타이머 관리에 대한 내용이 있지만 너무 길어 이곳에는 적지 않는다. // 

Pending Callbacks Phase
펜딩 큐를 관리하는 페이즈 (pending_cueue : 이전 이벤트 루프 반복에서 수행되지 못했던 콜백들이 담긴 큐.)
시스템 실행 한도 제한에 의해 처리하지 못한 작업들을 실행하는 페이즈.
pending_cue가 비어있다면 0을 리턴하고 다음 페이즈로
시스템 실행 한도를 넘기기 전까지 큐에 있는 보든 콜백을 순서대로 실행. 1을 리턴.

Idle, Prepare Phase
Idle은 매 tick마다, Prepare는 매 Polling 직전에 실행 -> 내부 관리용으로만 사용

Poll Phase
새로운 I/O 이벤트를 다루며 watcher_queue의 콜백들을 실행.(거의 모든 콜백들이 담기는 큐)
"시스템 실행 한도의 영향을 받는다."
+ 큐에 담긴 순서대로 I/O작업이 완료되어 콜백 또한 차례대로 실행된다는 보장이 없다.
Pool Phase Blocking 
남은 작업이 있는지 확인
watch_queue 즉시 실행할 수 있는 타이머 존재 : 다음페이즈로
watch_queue n초 이후 실행할 수 있는 타이머 존재 :
- setImmediate() : poll종료 후 check단계로
- setImmediate()아니면 : callback에 큐가 추가되기 기다린 후 즉시 실행(n초 기다린 후 다음 페이즈로)
(대기시간 timeout = block하는 시간. 

timeout = 0 : 요청이 없더라도 즉시 반환,

timeout > 0 : 요청이 없으면 생길때 까지 block, 요청 완료 or timeout초과시 완료된 FD의 수만큼 반환,

timeout =-1 : timeout없이 요청 완료까지 최대 30분까지 block

)

Check Phase
setImmediate의 콜백만을 위한 페이즈
setImmediate호출시 check phase의 큐에 담기고, js가 페이즈 진입시 차례대로 실행
process.nextTick은 같은 페이즈에서 호출한 즉시 실행 -> 진짜 즉시
setImmediate는 다음 틱에서 실행.(check phase 진입 하면 실행) -> 다음 tick에서 실행
-> 기능과 이름이 반대로 되어 있다. (하지만 수정이 불가능해 그대로 둔다고 한다...)

Close Callbacks Phase
시스템 실행 한도 초과 전까지 closing_handles에 담긴 작업  순서대로 실행

 

 

nextTickQeuue와 microTaskQueue

libuv가 아닌 node.js에 구현된 이벤트 루프 페이즈와 상관없이 동작.

nextTickQueue : process.nextTick()의 콜백 관리
microTaskQueue : resolve된 프라미스 콜백을 가지고 있다
지금 수행 작업이 끝나면 그 즉시 바로 실행.(nextTick이 microTask보다 먼저 실행)
이들은 시스템 실행 한도의 영향을 받지 않는다.

 

3. 생각 정리 

 

정리를 하긴 했지만, 사실 이해한 부분 보다는 이해못한 부분이 더 많다. 

각 페이즈의 자세한 설명은 아직까지 하지 못할것 같다.

단지 뭉뜽그려서 

node.js는 libuv라는 내부 라이브러리에 작업을 위임하는데, 이를 위한 구현체가 이벤트 루프이며,

이벤트 루프의 페이즈마다 호출 스택과 이벤트 큐의 작업 상태를 확인하여 비동기 작업이 실행되는 순서와 시점을 관리한다 정도로 정리할 수 있을것 같다.