본 글에서는, event loop 와 task queue 의 내부적인 동작과 순서에 대해서 설명합니다.
event loop 와 task queue 에 대해서 기본적인 지식을 어느정도 익힌 뒤, 읽는것을 추천합니다.

이해가 덜된 부분에, 개인적인 의견이 포함된 추측이 일부 포함되어 있습니다.
실제 동작이 아닌 부분이 포함되어 있을 수 있으니, 유의하여 필터링해서 보시기를 바랍니다.

  1. JS의 실행 환경 - Node.js / Browser

    • Node.js 는 libuv 로 이벤트 루프 기반 비동기 I/O 를 처리한다. (단일 스레드 기반의 이벤트 루프)
    • Browser 도 마찬가지로, 이벤트 루프 기반 비동기 I/O 를 처리한다. (뭐 쓰는지는 안찾아봄)
    • Node.js/Browser 는 V8 자바스크립트 엔진을 사용한다. (콜스택, 힙)
  2. JS 코드 실행 중, setTimeout 같은 친구들은 이벤트 루프를 통해서 스케쥴링 및 실행된다.
    (setTimeout 구문 자체는 콜백을 언제 실행할지 스케쥴링하고, 콜백 자체는 나중에 실행된다.)

  3. 이벤트 루프를 통해서 실행될 콜백들은 Node.js 환경 안에서 v8 context 를 통해 실행한다.


Node.js 에서 큐는 크게 세가지 타입으로 구성돼있다.

  1. next-tick queue
  2. micro-task queue (= job queue)
  3. task queues (= macrotask queue?, event queue, message queue)

task queues 는 libuv 의 event loop 를 실행하는 7가지 스텝에 걸쳐 분포돼있다.
next-tick queue, micro-task queue 는 libuv 에 포함되어있진 않지만, 이벤트 루프의 페이즈가 끝나는 시점(tick)에 next-tick queue > micro-task queue 순서로 쌓인 작업을 모두 처리한다.


My Questions

  • task queue 에 쌓인 이벤트 및 콜백은 worker thread(from thread pool) 혹은 kernel 에서 처리한다.

  • 보통 I/O 는 libuv 에서 Kernel 비동기 인터페이스를 사용해서 처리하지만 File I/O의 경우는 비동기로 플랫폼 통합 인터페이스를 구축하기 어려워서 thread pool 통해서 처리하기로 했다고 한다.

  • setTimeout/setInterval 타이머의 생성, 타임아웃된 콜백의 실행 작업은 무조건 timer phase 에서만 실행된다.
    (poll phase 에서 타이머 콜백이 실행된다고 적은 포스트가 많고, 공식 문서도 혼동되게 써놨는데 이부분에 대해서 명확한 답을 찾느라 한참 걸렸다. 결론은 아니다.)

  • libuv 디자인node.js 문서를 보면, poll phase 에서 I/O를 블록한다는 구문이 있던데 여기서도 뭔소린가 한참 헤맸다. (I/O를 처리하는 단계에서 I/O를 블록한다는 동작 자체가 말이 안된다고 생각이 들기도 하고, 원문을 봐도 번역이 틀린 것 같다.)
    내가 이해하기로는, I/O를 블록하는게 아니라 poll phase 에서 실행할게 없으면 실행할게 들어올때까지 I/O를 위해서 루프를 poll phase 블록한다는 이야기가 아닐까 추측한다. 그래서 Q&A 등록해놓음
    그렇다고 마냥 대기할 수 없으니, 다음 타이머의 대기 시간만큼만 기다리면서 들어오는 콜백들을 처리하고, 타이머로 넘어간다. 타이머로 넘어가기 직전 콜백에서 수행시간이 heavy할때 타이머가 지연되는 이슈가 이때문에 발생할 수 있다.
    타이머가 없다면 다른 작업들이 있는지도 좀 살펴보고.. 다른 작업들도 없다면 poll phase에서 Infinity 로 기다리는 뭐.. 그런게 아닐까? 로지컬하게 생각했을때, 이게 제일 말이 된다.

poll phase 에서는, 해당 페이즈에서 얼마나 대기를 할건지 계산하고 작업을 수행한다.

1. event loop 가 종료될 예정이라면
-- 타임아웃을 0초로 설정하고, 모든 I/O폴링 및 이벤트 콜백 처리를 수행하고 다음 phase로 이동한다.
3. pending i/o, close phase 에 처리할 작업이 있다면
-- 타임아웃을 0초로 설정하고, 모든 I/O폴링 및 이벤트 콜백 처리를 수행하고 다음 phase로 이동한다.
3. 수신할 I/O 콜백이 있다면(= 요청된 I/O 폴링이 있다면)
-- 타이머가 없다면 Infinity 로 설정, poll phase 에 I/O실행 콜백이 수신될때까지 대기
-- 대기시간을 만료될 타이머로 지정, 해당 시간까지 대기하면서 queue 처리하고 timer phase 로 이동
-- 타이머 페이즈로 이동 후, 루프에 I/O 이벤트 콜백이 들어왔다면 pending i/o phase 에서 실행된다.
4. poll queue 가 비었다면
-- 다음 phase 로 이동한다.

근데 또 libuv 디자인에 의하면 타이머에서 가져와 임계시간(타임아웃) 시간을 설정하고
다시 timer phase 로 돌아가는 로직은 뭐 어쩐건지..^^.. 못찾겠따..

공식문서부터 일단 헷갈리게 되어있고, 거기서 파생된 수많은 블로그들에서
짬뽕시켜놔서 납득 안되는 부분들도 상당히 많았기에 정보를 수집하고 올바르게 이해까지 가는데 힘들다..
(근데 내가 이해한것들도 딱 정확하지 않다는것도 함정)


프로그래밍에서 문제없을 정도라면 흐름을 파악해서 아래 micro/macro task 의 실행 순서 정도를 읽을 수 있는 수준정도면 된다고 본다.(이러한 순서의 보장은 node.js v11 이상부터, 브라우저와 동일한 동작을 보장한다.)

setTimeout(0) 과 setImmediate 의 실행 순서는 I/O 처리 이후가 아니면, 보장할 수 없다. (I/O 처리 이후에는 immediate 가 바로 실행)

// 비동기 요청을 하는 함수 - process.nextTick
// 콜백 - () => console.log(...)
function test() {
    setTimeout(() => console.log("timer task: set timeout"), 0);
    setImmediate(() => console.log("check task: set immediate"));
    Promise.resolve().then(() => console.log("micro task: promise"));
    queueMicrotask(() => console.log("micro task: queue micro task"));
    process.nextTick(() => console.log("tick: next tick"));
    console.log("call");
}

요약하면 구성도는 이렇게 되는 것 같다.

System kernel (OS, for use non-blocking I/O interfaces)
- network i/o
  
Node.js (program)
- node.js core
  - next-tick queue
    - <process.nextTick>
    - next-tick queue 는 event-loop phase 가 완료되고 모두 비워진다.
- v8 engine (c++ library)
  - heap
  - call stack
  - micro-task queue
    - <Promise, queueMicrotask>
    - micro-task queue 는 event-loop phase, next-tick queue 가 완료되고 모두 비워진다.
- libuv (c++ library)
  - event loop (=main thread=main loop=event thread)
    - task queues 
      1. timer
        - <setTimeout, setInterval>
        - 타이머를 설정해서 timer_heap 에 보관한다.
        - 실행 가능한 타이머들(타임 아웃된)을 꺼내서 콜백을 실행한다.
      2. pending i/o - 다음 루프로 지연된 I/O 콜백을 처리한다.
      3. idle handles - 내부용 콜백이 실행되는 큐
      4. prepare handles - 내부용 콜백이 실행되는 큐
      5. poll <--- non-blocking I/O ---> System kernel
        - 폴 단계에서 얼마의 시간동안 대기하고, 쌓인 큐를 처리할지 시간을 정한다.
          0초이면 non-blocking poll(단일 실행), 아니라면 blocking poll(반복) 이 된다.
          아래의 1,2 는 loop alive 가 false 면 0초로 판단하는 조건이다.
          아래의 3 은 처리할 I/O폴링이 없는 경우 0초로 판단하는 조건이다. (4는 뭔지 모르겠음)
          아래의 5,6 은 Phase를 넘겨 처리할 작업이 있으면 0초로 판단하는 조건이다.
          - 이벤트 루프가 UV_RUN_NOWAIT 플래그로 실행된 경우 - 0초
          - 이벤트 루프가 정지될 예정인 경우(uv_stop() 함수 호출된 경우) - 0초
          - 활성화된 handles 혹은 requests 가 없는 경우 - 0초
            - !uv__has_active_handles && !uv__has_active_reqs
            - I/O 이벤트 관련한것으로 보임
          - idle handles 중 하나라도 활성화 된 경우 - 0초
          - pending(i/o) callbacks phase 에 작업이 있는 경우 - 0초
          - close callbacks phase 에 작업이 있는 경우 - 0초
          - 타이머 O - 타임아웃이 가장 근처인 타이머의 타임아웃까지 남은 시간을 설정한다.
          - 타이머 X - Infinity(-1) 로 설정
        - watch_queue 에서 I/O폴링을 큐에서 모두 or 시스템 한도까지 진행한다.
        - watch_queue 로 처리한 폴링 이벤트들의 콜백을 모두 실행한다.
        - 대기시간이 0 이면 다음 phase 로 넘어간다.
        - 대기시간이 N 이면, N-loop_time 하고 다시 watch_queue 폴링으로 넘어간다.
        - 대기시간이 Infinity 면, queue 에 작업 갯수가 0인지 체크하고, 0이라면 다시 watch_queue 폴링으로 넘어간다.
      6. check handles 
        - <setImmediate>
        - setImmediate 콜백이 실행되는 큐
      7. close
        - close 처리에 대한 콜백이 실행되는 큐, 닫히기 때문에 가장 마지막 실행된다.
          아니라면 process.nextTick 으로도 실행할 수 있다.
        - e.g. socket.on('close', ...)
  - worker pool(=thread pool, 기본 4개)
    - worker thread
    - worker thread
    - worker thread
    - worker thread
    note) 커널에서 비동기 지원을 해주지 않는것들은 워커풀에서 수행된다.
      I/O-intensive
        - DNS
        - File System
      CPU-intensive
        - Crypto
        - Zlib

참고

node.js - uv_run
node.js - uv__io_poll
node.js - uv_backend_timeout
node.js - uv__next_timeout
node.js - timer_heap
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules
https://evan-moon.github.io/2019/08/01/nodejs-event-loop-workflow/
https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/
https://tk-one.github.io/2019/02/07/nodejs-event-loop/?fbclid=IwAR1Jp1CRlSkDkabK9QSpsuUUNx1CsMgRCsu9QflBAoGWXJJI6m2ZwVC3i6c
https://blog.insiderattack.net/javascript-event-loop-vs-node-js-event-loop-aea2b1b85f5c
https://www.clud.me/ced60995-fc4b-4ce0-b189-c477f985ed5d
http://docs.libuv.org/en/v1.x/design.html
https://hwangtaehyun.github.io/blog/node/event-queue/
https://ko.javascript.info/event-loop
https://meetup.toast.com/posts/89
https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

https://devidea.tistory.com/81

0개의 댓글