nodejs의 libuv는 어떻게 blocking 작업을 처리할까?
libuv?
socket과 entity의 high level 추상화를 위해 handles과 requests를 사용해 I/O polling 메커니즘을 구현해두었다.
- corss-platform support library
- event-driven 기반의 비동기 I/O 모델 (I/O는 시스템 커널의 것을 사용함)
- libuv의 모든 작업은 event-loop로 처리된다.
- event-loop는 single thread로 동작하고 callback으로 결과를 처리한다.
- handles: event loop 외부 혹은 유저의 행위에 의해 활성화되는 이벤트를 말하며, 특정 작업을 수행할 수 있는 long-live object. handles 객체에서 비동기 작업은 각각의 requests로 확인할 수 있다.
- requests: 작업의 시작과 완료에 대해 항상 callback을 호출하여 결과를 알려주는 short-term task. 보통은 1개의 callback을 호출한다. 일부(standalone reuqests)의 경우에는 handle을 타지 않고 바로 event-loop에서 처리한다.
UDP를 예로 들자면
uv_udp_t의 handler에는 uv_udp_send_t 라는 request가 있다.
uv_udp_t handler가 만들어지고, 데이터를 전송하는 비동기 작업이 이루어질 때, uv_udp_t에서는 uv_udp_send_t를 활용하여 network I/O에 대한 blocking 작업을 처리하게 되며, 작업에 대한 처리가 끝나면 uv_udp_send_t는 callback을 사용하여 데이터 전송에 대한 결과를 반환한다.
I/O loop (event loop)
libuv의 핵심 요소이다. single tread에서의 비동기 I/O 작업을 가능케 하는 것이 I/O loop의 목표이며, 이 I/O loop는 1개 thread에서 1개의 loop만 존재할 것을 가정한다. 때문에 I/O loop는 thread-safe하지 않다.
* 조금 더 자세히
OS의 지원 하에 처리될 수 있는 (network와 같은) 작업은 non-blocking socket을 통해 os 플랫폼에 처리를 요청한다. non-blocking socket은 커널의 interface를 통해 작업 요청이 들어가고 그 결과는 callback으로 받는다. blocking 작업의 경우 socket의 I/O활동을 기다리기 위해 I/O loop를 일시적으로 차단한다. I/O loop가 차단되면 poller와 callback을 등록하고 socket 상태(readable/writable)에 따라 callback이 호출되기까지 기다리며 결과는 마찬가지로 callback을 통해 처리된다. 이러한 과정을 이용하여 handles는 I/O 작업을 처리할 수 있게 된다.
- 초기화 및 타이머 실행
- loop 상태에 따라 아래를 처리
- 상태가 alive이면서 처리를 해야 할 handles가 있다면 handle에 필요한 requests를 활성화하거나 handles를 닫는다.
- 상태가 alive 아니면 종료
- pending callback을 호출한다 (이전 loop에서 callback 호출이 연기된 경우가 pending callback임) (모든 callback은 I/O polling 후에 호출됨)
- idle handle callback을 호출한다.
- prepare handle callback을 호출한다. (I/O 블럭을 준비하는 단계. 이 직후에 I/O를 위해 loop가 블럭된다)
- loop를 블럭할 poll timeout을 계산하고 이 시간만큼 loop를 블럭한다. 그리고 이 때 I/O와 관련된 handles가 실행되고 그에 대한 callback이 실행된다.
- handle callback이 호출되었는지 확인 (handle callback === prepare handle callback의 복제본)
- handle이 close된 경우 close callback 호출
- loop의 now를 업데이트하고 due timer를 실행, loop iteration 종료 후 2로 돌아감
* handler의 상태 변화 과정
idle -> prepare -> close <-|
-> pending ---
File I/O
file I/O의 경우에는 network I/O와는 다르게 platform에 의존할 수 없다고 한다. 때문에 blocking operation 으로 file I/O를 처리하며 이는 libuv의 thread-pool을 이용한다.
libuv에는 queue 형태로 동작하는 global thread pool이 있으며 file-system, dns function, uv_queue_work()를 통한 작업을 처리한다.
NodeJs에서 blocking 작업은 어떻게 처리될까?
blocking 작업은 javascript를 실행하는 중에 반드시 non-js operation이 있고, 이 작업이 완료될 때 까지 기다려야 하는 경우를 의미한다. single thread라고 생각해보면 이런 blocking 작업이 있다면 그 작업이 종료될 때 까지 기다리게 될 것이므로, 다른 작업을 수행할 수 없는 상태가 될 것이다.
현재의 대부분 커널은 multi thread환경을 가지고 있으며, 대부분의 작업들은 동시에 실행될 것이다. NodeJs는 single thread 작업 처리를 위해 커널로 blocking 작업을 socket을 통해 처리를 위임하고, poll queue에 callback을 추가하여 커널의 처리 결과를 다시 돌려받는다. 이러한 처리르 위해 NodeJs 표준 라이브러리가 제공하는 모든 I/O 메소드는 non-blocking이며 callback을 제공한다. 일부 메소드는 blocking 메소드와 비슷하지만 이런 기능등은 이름에 Sync가 마지막에 붙어 있다. (ex. ReadFile vs ReadFileSync)
Reference
thread safe: 멀티스레드 상황에서 하나의 스레드가 다른 스레드들의 상태 혹은 메모리의 상태에 영향을 받지 않고 온전히 실행될 수 있는 환경 Javascript Visualizer 9000 libuv design overview libuv basics NodeJs: event-loop NodeJs: Asynchronous Flow Control MDN: EventLoop
