Reactor Pattern

Node.js의 비동기 특성의 핵심인 Reactor패턴을 알아보자.

I/O는 속도가 느리다

I/O는 컴퓨터의 기본적인 동작 중에서 가장 느리다. I/O는 CPU측면에서 비용이 많이 들지 않지만, 요청을 보낸 순간부터 작업이 완료디는 순간까지 지연을 동반하게 된다.

Blocking I/O

전통적인 블로킹 I/O 프로그래밍에서는 I/O 요청에 해당하는 함수 호출은 작업이 완료될 때까지 스레드의 실행이 차단된다.

// 데이터를 사용할 수 있을 때까지 스레드가 블록
data = socket.read();
// 데이터 사용 가능
print(data)'

웹 서버에서 동시성을 처리하기 위한 전통적인 접근 방식은 처리해야하는 각각의 동시 연결에 대해 새로운 스레드 또는 프로세스를 시작하거나 풀에서 가져온 스레드를 재사용하는 것이다. 이렇게 해서 스레드가 I/O작업으로 차단되어도 분리된 스레드에서 처리되므로 다른 요청의 가용성에는 영향을 미치지 않는다.

image.png
위의 이미지는 각 스레드가 관련 연결로부터 새로운 데이터가 수신되기를 기다리는 유휴 상태에 중점을 두고 있다.
I/O는 메모리를 소비하고 컨텍스트 전환을 유발하므로, 각 연결 대해 대부분의 시간을 사용하지 않으면서 장시간 실행되는 스레드를 사용하는 것은 효율성 측면에서 최상의 절충안은 아니다.

Non Blocking I/O

최신 운영체제에는 non blocking I/O라고 하는 메커니즘을 지원한다. 시스템 호출은 데이터가 잃히거나 쓰여질때까지 기다리지 않고 항상 즉시 반환된다. 호출하는 순간에 결과를 사용할 수 없는 경우, 이 함수는 단순히 미리 정의된 상수를 반환하여 그 순간에 반환할 수 있는 데이터가 없음을 나타낸다.

예를들어, Unix에서 fcntl()함수는 기존 파일 디스크립터를 조작하여 운영 모드를 논 블로킹으로 변경하는데 사용한다. 이러한 종류의 논 블로킹 I/O에 액세스하는 가장 기본적인 패턴은 실제 데이터가 반환될 때까지 루프 내에서 리소스를 적극적으로 폴링(polling) 하는것 이다. 이것을 busy-waiting 이라고 한다. 하지만, 계속해서 polling을 함으로 인해 엄청난 양의 cpu 시간낭비를 초래한다.

이벤트 디멀티플렉싱

busy waiting은 논 블로킹 리소스를 처리하기 위한 이상적인 기술은 아니지만, 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스를 운영체제가 제공해준다. 감시된 리소스들로부터 들어오는 I/O이벤트를 수집해서 큐에 넣고 처리할 수 있는 새 이벤트가 있을 때까지 차단한다.

image.png

위의 그림은 busy-waiting기술을 사용하지 않고 단일 스레드 내에서 여러 I/O작업을 처리할 수 있는것을 보여주고 있다.

Reactor 패턴

image.png

  1. application은 이벤트 디멀티플렉서에 요청을 전달함으로써 새로운 I/O작업을 생성한다. 또한, application은 처리가 완료될 때 호출될 핸들러를 지정한다. (이벤트 디멀티플렉서에 새 요청을 전달하는 것은 논 블로킹 호출이며, 즉시 어플리케이션에 제어를 반환한다.)
  2. 일련의 I/O작업들이 완료되면 이벤트 디멀티플렉서는 새 이벤트를 이벤트 큐에 넣는다.
  3. 각 이벤트에 대해서 관련된 핸들러가 호출된다
  4. application 코드의 일부인 핸들러는 실행이 완료되면 이벤트 루프에게 제어권을 넘겨준다.
  5. 이벤트 큐 내의 모든 항목이 처리되면, 루프는 이벤트 디멀티플렉서에서 다시 블록되고 처리 가능한 새로운 이벤특라 있을 때 이과정이 다시 트리거 된다.

image.png