Node.js에서 타이머 구현
이전 블로그 게시물에서 언급했듯이 Node의 타이머는 새 스레드를 열어 구현하는 것이 아니라 이벤트 루프에서 직접 구현됩니다. 다음은 여러 JavaScript 타이머 예제와 Node 관련 소스 코드를 사용하여 Node.js에서 타이머 기능이 어떻게 구현되는지 분석합니다.
자바스크립트 타이머 기능의 특징
Node든 브라우저든 setTimeout과 setInterval이라는 두 가지 타이머 함수가 있으며, 그 작동 특성은 기본적으로 동일하므로 다음에서는 Node만 분석용 예시로 사용합니다.
우리는 JavaScript의 타이머가 컴퓨터의 기본 예약된 인터럽트와 다르지 않다는 것을 알고 있습니다. 인터럽트가 도착하면 현재 실행 중인 코드가 중단되고 예약된 인터럽트 처리 기능으로 전송됩니다. JavaScript 타이머가 만료되면 현재 실행 스레드에 실행 중인 코드가 없으면 해당 콜백 함수가 실행됩니다. 현재 실행 중인 코드가 있으면 JavaScript 엔진은 콜백을 실행하기 위해 현재 코드를 중단하지도 않습니다. start 새 스레드가 콜백을 실행하지만 현재 코드가 실행된 후에 처리됩니다.
console.time('A') setTimeout(function () { console.timeEnd('A'); }, 100); var i = 0; for (; i < 100000; i++) { }
위 코드를 실행해보면 최종 출력 시간이 100ms 정도가 아니고 몇 초 정도 되는 것을 알 수 있습니다. 이는 예약된 콜백 함수가 실제로 루프가 완료되기 전에 실행되지 않고 루프가 끝날 때까지 연기됨을 보여줍니다. 실제로 JavaScript 코드 실행 중에는 모든 이벤트를 처리할 수 없으며, 현재 코드가 완료될 때까지 새로운 이벤트를 처리해야 합니다. 이것이 시간이 많이 걸리는 JavaScript 코드를 실행할 때 브라우저가 응답하지 않는 이유입니다. 이러한 상황을 해결하기 위해 우리는 Yielding Processes 기술을 사용하여 시간이 많이 걸리는 코드를 작은 청크(청크)로 나누고, 각 청크가 처리된 후 setTimeout을 한 번 실행하고 짧은 시간 후에 다음 청크를 처리하는 데 동의할 수 있습니다. 이 기간 동안 브라우저/노드는 대기 중인 이벤트를 처리할 수 있습니다.
보충정보
고급 타이머 및 항복 프로세스는 22장 JavaScript 고급 프로그래밍의 고급 기술, 제3판에서 자세히 설명합니다.
노드에서 타이머 구현
libuv의 uv_loop_t 유형 초기화
이전 블로그 게시물에서는 Node가 이벤트 예약을 위해 default_loop_ptr을 시작하기 위해 libuv의 uv_run 함수를 호출한다고 언급했습니다. default_loop_ptr은 uv_loop_t 유형의 변수 default_loop_struct를 가리킵니다. Node가 시작되면 uv_loop_init(&default_loop_struct)를 호출하여 초기화합니다. uv_loop_init 함수의 발췌는 다음과 같습니다.
int uv_loop_init(uv_loop_t* loop) { ... loop->time = 0; uv_update_time(loop); ... }
루프의 시간 필드에 먼저 값 0이 할당된 다음 uv_update_time 함수가 호출되어 가장 최근의 계산 시간이 loop.time에 할당되는 것을 볼 수 있습니다.
초기화가 완료된 후 default_loop_struct.time에는 초기값이 있으며, 시간 관련 연산은 이 값과 비교하여 해당 콜백 함수를 호출할지 여부를 결정합니다.
libuv의 이벤트 스케줄링 핵심
앞서 언급했듯이 uv_run 함수는 이벤트 루프를 구현하는 libuv 라이브러리의 핵심 부분입니다.
다음은 타이머와 관련된 위 로직에 대한 간략한 설명입니다.
현재 루프 개념에서 "지금"을 표시하는 현재 루프의 시간 필드를 업데이트합니다.
루프가 살아 있는지 확인하세요. 즉, 루프에서 처리해야 하는 작업(핸들러/요청)이 있는지 확인하세요. 그렇지 않으면 루프할 필요가 없습니다.
등록된 타이머를 확인하세요. 타이머에 지정된 시간이 현재 시간보다 늦다면 타이머가 만료된 것이며 해당 콜백 함수가 실행된다는 뜻입니다.
I/O 폴링을 수행합니다(즉, 스레드를 차단하고 I/O 이벤트가 발생할 때까지 대기). 다음 타이머가 만료될 때 I/O가 완료되지 않으면 대기를 중지하고 다음 타이머의 콜백을 실행합니다.
I/O 이벤트가 발생하면 해당 콜백이 실행되는데, 콜백 실행 시간 동안 다른 타이머가 만료되었을 수 있으므로 타이머를 다시 확인하여 콜백을 실행해야 합니다.
(실제로 (4.) 여기가 더 복잡합니다. 단지 한 단계의 작업이 아닙니다. 이 설명은 다른 세부 사항을 포함하지 않고 타이머 구현에만 중점을 두기 위한 것입니다.)
노드는 루프가 더 이상 활성화되지 않을 때까지 uv_run을 계속 호출합니다.
노드의 timer_wrap 및 타이머
Node에는 TimerWrap 클래스가 있는데, Node.js 내부에 timer_wrap 모듈로 등록되어 있습니다.
NODE_MODULE_CONTEXT_AWARE_BUILTIN(timer_wrap, node::TimerWrap::Initialize)
TimerWrap 클래스는 기본적으로 uv_timer_t를 직접 캡슐화한 것이며 NODE_MODULE_CONTEXT_AWARE_BUILTIN은 Node에서 내장 모듈을 등록하는 데 사용하는 매크로입니다.
이 단계 후에 JavaScript는 이 모듈을 가져와서 작동할 수 있습니다. src/lib/timers.js 파일은 JavaScript를 사용하여timer_wrap 함수를 캡슐화하고exports.setTimeout,exports.setInterval,exports.setImmediate 및 기타 함수를 내보냅니다.
노드 시작 및 전역 초기화
이전 기사에서는 Node가 시작될 때 실행 환경 LoadEnvironment(env)를 로드한다고 언급했습니다. 이 함수에서 매우 중요한 단계는 src/node.js를 로드하고 이를 실행하는 것입니다. 모듈 그리고 전역 및 프로세스를 초기화합니다. 물론 setTimeout과 같은 함수도 src/node.js에 의해 전역 개체에 바인딩됩니다.
위 내용은 이 글의 전체 내용입니다. 모두 마음에 드셨으면 좋겠습니다.