PracticeEveryday

Nodejs 본문

Nodejs

Nodejs

kimddakki 2022. 5. 14. 15:01
이벤트 루프의 여러 페이즈

 - 이벤트 루프는 말했든 여러 페이즈로 구성되어 있고 페이즈마다 관심있어 하는 작업이 다르다.

 - 비동기 작업의 종류마다 담기는 페이즈가 달라지고 실행 순서 또한 달라진다.

 

1. Timer Phase

 - Timer Phase 는 말 그대로 setTimeout이나 setInterval과 같은 함수가 만들어내는 타이머들을 다룬다.

 - // 엄밀하게 말하면 Timer Phase가 관리하는 큐에 콜백을 직접 담지는 않는다.

 

 - Timer Phase는 setTimeout이 호출 되었을 때 타이머의 콜백을 큐에 저장하지 않는다.

 - 그 대신 콜백을 언제 실행할 지에 정보가 담긴 타이머를 Timer Phase가 관리하는 min-heap에 넣는다.

 - 만약 Poll Phase에서 setTimeout을 3번 호출 했다면 Timer Phase의 min-heap에 3개의 타이머가 저장되게 된다.

 - 타이머를 실행할 준비가 되면(시간이 되면) 타이머가 가리키고 있는 콜백을 호출한다.

더보기

여러 블로그에서 Timer Phase는 타이머만 관리하고 Poll Phase에서 콜백이 실행된다고 기술했다. 하지만 이는 사실이 아니다. Timer Phase에서 타이머를 검사하고 실행도 한다. 실제 예제는 아래 코드에서 알아보자. 이에 대한 Github도 존재한다.

 

 - 이벤트 루프에서 Node.js는 현재 페이즈가 관리하는 작업들만 실행할 수 있다. 따라서 Node.js는 Timer Phase에서만 타이머를 검사한다.

 - 즉 Node.js가 Timer Phase에 진입해야만 타이머들이 실행될 기회를 얻는다.

 - 따라서 우리가 Poll Phase에서 setTimeout(fn, 1)을 호출한다고 해도 Node.js는 정확히 1ms 뒤에 콜백이 실행됨을 보장하지 않는다.

 - Timer Phase에 진입하는 데 1초가 걸린다면 타이머의 콜백을 실행하는 데는 1ms이 아니라 1초 이상이 걸리게 된다!!

 

 => 현재 시간을 now라고 했을 때 setTimeout(fn, delay)는 now + delay에 fn이 실행됨을 보장하지 않는다.

 => 적어도 now + delay 이후에 fn이 실행됨을 보장한다.

 => 또한 Timer Phase는 큐에 있는 모든 작업을 실행하거나 시스템의 실행한도에 다다르게 되면 다음 페이즈로 넘어간다.

 

Timer Phase는 min-heap을 이용해서 타이머를 관리한다. 이 덕분에 실행 시간이 
가장 이른 타이머를 효율적으로 찾을 수 있다.

Timer Phase는 SetTimeout(fn, 1000)을 호출했다고 해서 정확하게 1초 후에 fn이 호출됨을 보장하지 않는다.
다르게 말하면 1초 이상의 시간이 흘렀을 때 fn이 실행됨을 보장한다.

큐에 있는 모든 작업을 실행하거나 시스템의 실행 한도에 다다르면 다음 페이즈인 
Pending Callbacks Phase로 넘어간다.

2. Timer Phase의 타이머 관리

 - 현재 시간을 now라고 하자. setTimeout(fn, delay)가 실행되면 Node.js는 타이머를 min-heap에 저장한다. 이 때 setTimeout을 호출한 시간을 registeredTime이라고 하면

 - Node.js가 Timer Phase에 진입하면 min-heap 에서 타이머를 하나 꺼낸다.

 - 그 타이머에 대해 now - registeredTime >= delay 조건을 검사한다.

 - 조건을 만족한다면 타이머를 실행할 준비가 되었으므로 타이머의 롤백을 실행한다.

 - 그리고 다시 min-heap에서 타이머를 꺼내 검사한다.

 - 조건이 성립하지 않는다면 남은 타이머들을 검사하지 않고 다음 페이즈로 넘긴다.

 - min-heap이 타이머를 오름차순으로 관리해 주기 때문에 나머지를 검사할 필요가 없기 때문이다!!

1. Node.js가 Timer Phase에 진입해 min-heap으로 부터 가장 이른 타이머를 요청한다.

2. min-heap은 가장 이른 타이머를 반환해 준다.  = 타이머 B

3. Node.js는 타이머를 현재 실행 할 수 있는지 확인한다.

 => now(18) - registeredTime(10) >= delay(5) === true 타이머 실행 가능

4. Node.js는 min-heap에서 타이머 b를 제거하고 B와 연결된 콜백을 실행한다!

4. 이어서 Node.js는 다시 min-heap에게 가장 이른 타이머를 요청한다. // 타이머 A

5. Node.js는 타이머 A를 현재 실행 할 수 있는지 확인한다.

 => now(18) - registeredTime(20) >= delay(5)가 성립하지 않으므로 타이머를 실행할 수 없다.

6. Node.js는 타이머를 실행하지 않는다.

7. 가장 이른 타이머를 실행할 수 없으므로 min-Heap에 존재하는 모든 타이머를 실행할 수 없음이 당연하다.

8. Node.js는 더이상 타이머를 요청하지 않고 다음 페이즈로 넘긴다!! // min-heap에 특성 덕에 가능하다.

 

const now = Date.now();
const fn = function () {
  const now2 = Date.now();
  console.log(now2 - now);
};

const A = setTimeout(fn, 50); // 62
const B = setTimeout(fn, 150); // 152
const C = setTimeout(fn, 200); // 213
const D = setTimeout(fn, 500); // 500
const E = setTimeout(fn, 3000); // 3014

 - delay가 50, 150, 200, 500, 3000인 5개의 타이머를 0초에 등록했을 때

 - 타이머는 min-heap에 아래와 같이 저장되어 있다고 생각 할 수 있다.

    ( 이진 트리 구조를 가져야 하지만 편의를 위해 단순이 오름차순으로 구현했다.)

Case 1 : 모든 타이머를 검사할 필요가 없는 경우

 - Nodejs가 30ms에 Timer Phase에 진입했을 경우 Node.js는 min-heap에서 A를 꺼내 검사한다.

 - A의 delay는 50으로 now(30 - registeredTime(0) >= delay(50)이 성립하므로 A의 콜백이 실행되지 않는다.

 - Node.js는 min-heap의 오름차순 특성 덕분에 뒤의 B, C D, E를 검사할 필요가 없다.

 - A를 실행할 수 없으면 뒤의 타이머들은 당연히 실행할 수 없기 때문이다.

 - Node.js는 다음 페이트로 넘어간다.

 

Case 2 : 일부 타이머를 실행할 수 있는 경우

 - Node.js가 170ms 에 Timer Phase에 진입했을 경우 Node.js는 min-heap에서 A를 꺼내 검사한다.

 - now(170) - registeredTime(0) >= delay(50) 이므로 A의 콜백을 실행하고 A의 타이머를 heap에서 제거한다.

 - 그 다음 B를 꺼내 검사한다.

 - now(170) - registeredTime(0) >= delay(150) 이므로 B의 콜백을 실행하고 B의 타이머를 heap에서 제거한다.

 - 그 다음 C를 꺼내 검사한다.

 - now(170) - registeredTime(0) >= delay(200)은 false 이므로 C의 콜백을 실행하지 않고 다음 페이즈로 넘어간다.

 

Case 3: 모든 타이머를 실행할 수 있음에도 다음 페이즈로 넘어가는 경우

 

 - 이 때 Timer Phase는 시스템의 실행 한도에 영향을 받는다는 것을 주의해야 한다.

 - 실행 할 수 있는 타이머가 남아 있더라도 시스템 실행 한도에 다다르면 다음 페이즈로 넘어간다.

 - 시스템의 실행한도가 3이고 Node.js가 Timer Phase에 1000에 진입했다고 했을 때

 - Node.js는 타이머 D까지 실행 할 수 있음에도 A, B, C까지만 실행하고 다음 페이즈로 넘어간다.

 


3. TIme Phase의 실제 코드

// 이벤트 루프의 시간을 업데이트하는 함수
UV_UNUSED(static void uv__update_time(uv_loop_t* loop)) {
  /* Use a fast time source if available.  We only need millisecond precision.
   */
  loop->time = uv__hrtime(UV_CLOCK_FAST)/1000000;
}

 - uv__hrtime 함수를 호출해 현재 시간을 얻고 ms 단위로 변경해 loop->time에 저장한다.

 - 여기서 주의해야 할 점은 이벤트 루프에서 현재 시간을 사용 할 때 코드를 실행하는 시점의 시간을 사용하지 않고

    uv__update_time을 호출했을 때 loop -> time에 저장된 시간을 사용한다는 점이다!

void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;
​
  for (;;) {
    heap_node = heap_min(timer_heap(loop)); // 힙에서 타이머를 꺼낸다
    if (heap_node == NULL){
      break; // 노드가 Null 이면 bradk한다
    }
​
    handle = container_of(heap_node, uv_timer_t, heap_node);
    if (handle->timeout > loop->time){ // 만약 타이머의 콜백을 호출할 시간이 안되었다면 Timer Phase를 종료한다
      break;
    }
​
    uv_timer_stop(handle);
    uv_timer_again(handle);
    handle->timer_cb(handle); // 타이머의 콜백을 호출한다
  }
}

 - 우선 min-heap에서 타이머를 꺼낸다. min-heap은 오름차순 정렬 되어 있으므로 당연히 가장 이른 타이머다.

 - 만약 타이머에 저장된 timeout이 이벤트 루프의 현재 시간 보다 크다면 실행할 준비가 안 되어 있으므로 TImer Phase를 종료한다.

 - 하나라도 조건을 만족하지 않는 타이머를 발견하는 즉시 Timer Phase를 종료한다.


 

 

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

글에 들어가기에 앞서 Node.js의 이벤트 루프의 경우 공식 문서에 설명이 부족하고 이에 따라 여러 사람들이 각자 나름대로 분석한 글이 많아 무엇이 이벤트 루프의 정확한 동작인지 알기 힘듭니

www.korecmblog.com

 

'Nodejs' 카테고리의 다른 글

Nodejs  (0) 2022.05.14
Nodejs  (0) 2022.05.14
Nodejs  (0) 2022.05.14
Nodejs  (0) 2022.05.14
Nodejs  (0) 2022.05.13
Comments