본문 바로가기

프로그래밍

Proactor pattern

출처 : 온라인서버제작자모임



출처

원문 : http://www.cse.wustl.edu/~schmidt/PDF/proactor.pdf

번역문 : http://www.redwiki.net/wiki/wiki.php/Proactor

위 URL에 있던 문서에서 뒷부분을 추가 번역했습니다.

개요

현대의 운영체계들은 동시실행에 기초한 어플리케이션(서버)를 개발하는 것을 지원하기 위해 여러가지 매커니즘들을 제공한다. 동기적 멀티쓰레딩은 여러 동작이 동시다발적으로 실행되는 어플리케이션을 개발하는데 있어서 인기있는 매커니즘이다. 그렇지만 쓰레드는 종종 높은 성능 과부하를 가질 때가 있고, 동기화 패턴과 법칙들에 대한 깊은 지식을 요구한다. 그러므로, 여러 운영체계들은 동시실행 처리에 있어서 멀티쓰레딩의 과부하나 복잡도의 대부분을 완화시키는 장점이 있는 비동기적 매커니즘을 지원한다.

이 논문에서 제시하는 proactor 패턴은 운영체계에 의해 제공되는 비동기 매커니즘들을 효율적으로 다루는 어플리케이션과 시스템을 구축하는 법에 대해서 설명하고 있다. 어플리케이션이 비동기적인 작업을 수행할 때, 운영체계는 어플리케이션을 대신하여 작업을 실행한다. 이것은 어플리케이션으로 하여금 필요한 만큼의 수의 쓰레드를 가질 필요없이 동시에 동작하는 다수의 동작을 실행할 수 있게 해준다. 따라서, proactor 패턴은 서버 프로그래밍을 단순화 시켜주며, 비동기 처리를 위한 운영체계의 지원에 의존하고 보다 적은 쓰레드를 요구하게 됨으로써 성능을 증진시켜주게 된다.

취지

proactor 패턴은 비동기 이벤트들의 완료시점에 실행되는 다중 이벤트 핸들러들의 디스패칭과 디멀티플랙싱을 지원한다. 이 패턴은 완료 이벤트들의 디멀티플랙싱을 통합하고 그에 알맞은 이벤트 핸들러들을 디스패칭하는 것을 지원함으로써 비동기기반의 어플리케이션 개발을 단순화해준다.

동기

이 섹션은 proactor 패턴을 사용하기 위한 개념과 동기를 기술한다.

고성능 서버의 의미과 능력

동기적인 방식의 멀티쓰레드 혹은 반응적인(reactive) 프로그래밍상에서 제약없이 동시처리방식으로 작업들을 실행시키려 하는데 성능상의 잇점이 요구될 때에는 proactor 패턴을 사용한다. 이 잇점들을 설명하기 위해서 동시처리방식으로 다중의 처리를 실행할 필요가 있는 네트워크 어플리케이션을 고려하자. 예를 들자면, 고성능의 웹서버는 반드시 여러개의 클라이언트 [1, 2]로부터 보내어진 HTTP 요청들을 동시에 처리해야만 한다. "Figure 1"은 웹브라우져들과 웹서버사이의 전형적인 상호작용관계를 보여준다. 사용자가 브라우져에게 URL을 열도록 지시하면, 브라우져는 HTTP GET 요청을 웹 서버로 보낸다. 요청을 접수하면 서버는 요청을 파싱하고 적법한지 검사한 후, 지정된 화일(들)을 브라우져로 되돌려 보낸다.

Figure 1. 일반적인 웹서버의 구조

고성능의 웹서버가 개발되기 위해서는 다음 능력들을 가져야한다:

  • 동시처리(Concurrency); 서버는 동시에 여러개의 클라이언트 요청을 수행해야만 한다.
  • 효율성(Efficiency); 서버는 지연(latency)을 최소화해야하고, 대역폭을 최대로 활용해야 하며, 불필요하게 CPU(들)을 동작시키는 것을 피해야한다.
  • 프로그래밍 단순화(Programming simplicity); 서버의 디자인은 효율적인 동시처리에 대한 운영 전략의 적용을 단순화할 수 있어야 한다.
  • 적응성(Adaptability); 신규 혹은 개선된 트랜스포트 프로토콜(HTTP 1.1과 같은)을 지원하는데 있어서 최소한의 관리 비용이 들도록 해야한다.

웹서버는 몇가지 동시처리 전략(다중 동기화된 쓰레드방식, reactive한 동기적 이벤트 디스패칭, proactive한 비동기 이벤트 디스패칭)들을 사용하여 구현될 수 있다. 아래에서 우리는 전통적인 접근 방식의 결점을 찾아보고 어떻게 proactor 패턴이 고성능의 서버 어플리케이션에 대한 효율적이고 유연한 비동기 이벤트 디스패칭 전략을 지원하는 강력한 태크닉을 제공하는지 살펴볼 것이다.

전통적인 동시처리방식 서버모델들의 일반적인 덫과 함정들

동기화된 멀티쓰레딩과 reactive한 프로그래밍은 동시처리를 구현하는 일반적인 방법이다. 이 장은 이 프로그래밍 모델들에 대한 결점들을 설명한다.

다중 동기화 쓰레드를 통한 동시처리

아마도 대부분의 동시처리방식의 웹서버를 구현하는 직관적인 방법은 동기적인 멀티쓰레딩 방식이다. 이 모델에서는 다중 서버 쓰레드들이 동시에 여러 클라이언트로 부터 HTTP GET 요청을 처리한다. 각 쓰레드는 접속 구축을 실행하고, HTTP 요청을 읽고, 요청을 파싱하며, 화일 전송 처리를 동기적으로 수행한다. 결과적으로 각 처리는 해당 처리가 완료될 때 까지 블럭당한다.

동기화된 쓰레딩 방식의 주된 잇점은 어플리케이션 코드의 단순함이다. 특별한 경우, 클라이언트 A의 요청을 서비스하기위해 웹서버에 의해 실행되는 처리들은 클라이언트 B의 요청을 서비스하기 위한 처리와는 대부분 독립적이다. 따라서, 쓰레드간에 공유되는 상태들의 양이 적기 때문에 별도의 쓰레드상에서 서로 다른 요청들을 서비스하는 것이 쉬운 것이다. (이것은 동기화의 필요성을 최소화하는 요인이다) 게다가, 별도의 쓰레드상에서 어플리케이션 로직을 실행하는 것은 개발자로 하여금 순차적인 명령들과 블록킹 처리를 다루는 것을 허용한다.

Figure 2. 멀티스레드 웹서버 구조


"Figure 2"는 어떻게 동기적인 쓰레드를 사용하여 디자인된 웹서버가 여러개의 클라이언트들을 동시실행방식으로 처리할 수 있는지 보여준다. 이 figure는 Sync Acceptor가 동기적으로 네트워크 접속을 accept 처리하기위한 서버측 구조를 은폐하고(encapsulate)있다는 것을 보여준다. "연결 1개당 쓰레드 1개" 방식을 사용해서 HTTP GET 요청을 서비스하기위한 각각의 쓰레드들의 실행단계는 다음과 같이 요약될 수 있다:

  1. 각 쓰레드는 accept()함수실행시 클라이언트 접속요청이 올때까지 동기적으로 블록당한다.
  2. 클라이언트가 서버에 연결되면, 접속이 accept된다. (블럭이 풀린다)
  3. 새로 접속된 클라이언트의 HTTP 요청이 동기적으로 네트워크 연결을 통하여 읽혀진다.
  4. 요청을 파싱한다.
  5. 요청된 화일을 동기적으로 읽는다.
  6. 화일의 내용이 동기적으로 클라이언트에게 전송된다.

동기적 쓰레드 모델을 적용한 웹서버를 C++ 코드 예제를 부록 A.1에 첨부해놓았다. 앞에서 기술했던 것처럼, 각각에 연결된 클라이언트는 전담(dedicated) 서버 쓰레드에 의해 동시처리적으로 서비스된다. 쓰레드는 다른 HTTP 요청을 서비스하기 전에 동기적으로 요청받은 처리를 완료한다. 그러므로, 여러 클라이언트에 대해 서비스하는동안 동기적 입출력을 실행하려면, 웹서버는 다중 쓰레드를 생성해야만 한다. 이 동기적 멀티쓰레드 모델이 직관적이고, 비교적 효과적으로 다중 CPU 체계에서 매핑된다고 할지라도, 이것은 다음과 같은 단점들을 가진다:

  • 쓰레딩 정책이 동시처리 정책과 강하게 연관되어있다. : 이 구조는 각 연결된 클라이언트들을 위한 개별적인 전담 쓰레드를 필요로 한다. 동시처리방식의 어플리케이션은 동시에 서비스되는 클라이언트의 수보다는 사용가능한 자원(쓰레드 풀링을 통한 CPU의 수와 같은 것)에 쓰레딩 전략을 맞추는 것에 의해 보다 더 최적화될 수 있다.
  • 동기화의 복잡도 증가 : 쓰레드 처리는 - 서버의 공유 자원들(캐쉬된 화일이나 웹 페이지의 히트수 기록등등)에 대한 억세스를 직렬화하기위해서 필요한 - 동기화 체계에 대한 복잡도를 증가시킨다.
  • 성능상의 과부하 증가 : 쓰레드 처리는 컨택스트 스위칭, 동기화, CPU간의 데이타 이동등에 기인하여 과부하상태로 실행될 수 있다.
  • 비호환성: 쓰레드는 모든 운영체계에 가능하지 않을 수도 있다. 게다가, 운영체계는 선점형 그리고 비선점형 쓰레드의 견지에서 보았을 경우 상당히 차이가 있다. 이런 이유로, 운영체계 플렛폼에 상관없이 동일하게 동작하는 다중 쓰레드방식의 서버를 만드는 것은 여려운 일이다.


이런 단점들 때문에, 멀티쓰레딩은 동시처리 방식의 웹서버를 개발하는데 있어서는 종종 아주 효율적이지도 않고 구조가 그리 간단하지도 않은 솔루션이 되고 있다.

반응적(reactive) 이벤트 디스패칭을 통한 동시처리

또다른 동기적방식의 웹서버를 구현하는 일반적인 방법은 반응적(reactive) 이벤트 디스패칭 모델을 사용하는 것이다. reactor 패턴은 어떻게 어플리케이션이 Initiation Dispatcher를 사용하여 이벤트 핸들러를 등록할 수 있는지 보여준다. Initiation Dispatcher는 블록킹 없이 명령이 입회(initiate)가능할 경우 그에 알맞는 이벤트 핸들러를 알려준다. The Initiation Dispatcher notifies the Event Handler when it is possible to initiate an operation without blocking. 싱글쓰레드 기반의 동시처리 방식 웹서버는 reactive 이벤트 디스패칭 모델을 사용할 수 있다. (이 모델은 reactor가 알맞은 명령이 들어왔음을 알려줄 때 까지 이벤트 루프상에서 기다리는 구조를 가진다.) 웹서버상의 reactive 명령에 대한 예는 Initiation Dispatcher을 사용한 acceptor의 등록작업이다. 데이타가 네트워크 연결을 통해서 도착하면, 디스패쳐는 acceptor를 콜백한다. acceptor는 네트워크 연결을 수락하고 HTTP 핸들러를 생성한다. 그런 다음 이 HTTP 핸들러는 웹서버의 싱글쓰레드 제어하에서 방금 진행되는 연결로 전송되어온 URL 요청을 처리하기 위해 reactor에 등록된다.


figure 3과 4는 반응적 이벤트 디스페칭을 사용하여 디자인된 웹서버가 여러개의 클라이언트를 어떻게 다루는지를 보여준다. figure 3는 웹서버로 클라이언트가 접속할때 밟게되는 단계를 보여주며, figure 4는 웹서버가 어떻게 클라이언트 요청을 처리하는지 보여준다.

Figure 3. 클라이언트가 Reactive 웹서버에 접속


figure 3의 단계는 다음과 같이 요약될 수 있다:

1. 웹서버는 신규 접속을 accept처리하기 위한 Initiation Dispatcher에 acceptor를 등록한다. (The Web Server registers an Acceptor with the Initiation Dispatcher to accept new connections.)
2. 웹서버는 Initiation Dispatcher의 이벤트루프를 동작시킨다.
3. 클라이언트가 웹서버에 접속한다.
4. acceptor가 신규 접속의 발생여부를 Initiation Dispatcher에게 알려주고 acceptor는 신규 접속을 받아들인다.
5. acceptor는 신규 클라이언트에 서비스하기위해 HTTP 핸들러를 생성한다.
6. HTTP 핸들러는 클라이언트의 요청 데이타를 읽기위해 Initiation Dispatcher에 접속정보를 등록한다. (다시말하면, 접속상태를 "읽기대기"모드로 설정한다.)
7. HTTP 핸들러 서비스는 신규 클라이언트로부터의 요청에 따라 서비스를 시작한다.


Figure 4. 클라이언트가 HTTP Request를 Reactive 웹서버에 요청


그림 4는 reactive 웹서버가 HTTP GET 요청을 서비스하는 단계를 보여준다. 그 과정은 다음과 같다:

1. 클라이언트는 HTTP GET 요청을 전송한다.
2. 클라이언트의 요청 데이타가 서버에 도착하였을때 Initiation Dispatcher는 HTTP 핸들러에게 그 사실을 알려준다.
3. 요청 데이타가 비블록상태로 읽혀진다. (이것은 읽기명령이 즉시 수행을 끝내지 못했을 경우 EWOULDBLOCK을 반환하는 상태를 말한다.) HTTP 요청데이타가 완전히 읽혀질때까지 2번과 3번단계를 반복한다.
4. HTTP 핸들러는 HTTP 요청을 파싱한다.
5. 요청된 화일을 동기적으로 화일 시스템으로 부터 읽는다.
6. HTTP 핸들러는 데이타를 보내기위해 Initiation Dispatcher에 연결정보를 등록한다. (다시 말하면, 접속상태가 쓰기대기상태로 된다.)
7. Initiation Dispatcher는 TCP 접속이 쓰기모드 상태라는 것을 HTTP 핸들러에게 알려준다.
8. 요청 데이타가 클라이언트에게 비블록상태로 전송되어진다. (이것은 쓰기명령이 즉시 수행을 끝내지 못했을 경우 EWOULDBLOCK을 반환하는 상태를 말한다.) HTTP 요청데이타가 완전히 전송되어질때까지 7번과 8번단계를 반복한다.


reactive 이벤트 디스페칭 모델이 적용된 웹서버에 대한 C++ 코드 예제가 부록 A.2에 첨부되어있다. Initiation Dispatcher가 별도의 단일 쓰레드 상에서 실행되고 있기 까닭에 네트워크 입출력 명령들이 블록당하지 않는 상태로 reactor의 제어 아래에서 실행된다. If forward progress is stalled on the current operation, the operation is handed off to the Initiation Dispatcher, which monitors the status of the system operation. 명령이 다시 우선 처리되는 상황이 오면, 알맞은 이벤트 핸들러에게 이 사실이 알려지게 된다.


reactive 모델의 주된 장점은 이식성, coarse-grained 동시처리 제어에 따른 낮은 과부하 (다시말하면, 싱글스레드 방식은 동기화나 컨텍스트 스위칭이 요구되지 않는다.), 디스페칭 체계로 부터 어플리케이션 로직을 분리함으로서 얻을수 있는 모듈화의 잇점들을 들 수 있다. 그럼에도 불구하고, 이 방식은 다음과 같은 단점들을 가지고 있다:


프로그램 복잡도의 증가 : 앞에서 언급했듯이, 서버가 특정 클라이언트에 대해 블록당하지 않고 서비스를 실행하려면 프로그래머는 복잡한 로직을 작성해야만 한다.

  • 멀티쓰레딩에 대한 운영체계 지원의 부족 : 대부분의 운영체계들은 reactive 디스페칭 모델을 select() 함수로 구현한다[7]. 어쨌거나, select()는 같은 descriptor set에서 한개이상의 쓰레드의 사용을 허용하지 않는다. 이것은 reactive 모델이 고성능의 서버의 제작에는 맞지 않다는 의미가 된다. (하드웨어 병렬처리를 효율적으로 이용하려면 멀티쓰레드의 사용은 필수적이기 때문이다.)
  • 실행가능한 task들의 스케줄링: 선점형 쓰레드를 지원하는 동기방식의 멀티쓰레딩 구조하에서는, 설치된 CPU를 가지고 실행가능한 쓰레드들을 스케줄하고 시분할하여 제어하는 것은 운영체계의 역할이라고 할 수 있다. 이 스케줄링은 어플리케이션에서 한개의 쓰레드만이 존재하는 reactive 구조에서는 사용될 수 없다. 그러므로, 시스템 개발자는 웹서버에 연결된 모든 클라이언트들의 요청을 처리하는데 있어서 이 1개의 쓰레드의 실행단위를 주의깊게 시분할할 필요가 있다. 이것은 짧은 주기로 비블록 명령들을 실행함으로서 구현될 수 있다.


요약하면, 이런 단점들 때문에 reactive 모델은 하드웨어 병렬처리가 지원된다면 그렇게 높은 효율을 기대할 수 있는 모델이 아니다. 이 모델은 또한 서버를 코딩하는 데 있어서 흔히 요구되는 입출력의 블록킹 회피를 구현하려면 다소 높은 수준의 프로그래밍 복잡도를 극복해야만 한다.

해결책 : proactive 처리를 통한 동시처리

OS 플렛폼이 비동기 명령들을 지원할 경우, 고성능의 웹서버를 구현하는 효율적이고 편리한 방법은 proactive 이벤트 디스페칭을 사용하는 것이다. proactive 이벤트 디스페칭을 사용하여디자인된 웹 서버는 한개이상의 쓰레드를 제어하여 비동기명령의 완료여부를 다루는 것이 가능하다. 따라서, proactor 패턴은 완료 이벤트 디멀티플렉싱과 이벤트 핸들러 디스페칭을 통합함으로써 비동기방식의 웹서버 구조를 단순화시킨다.


비동기 웹서버는 운영체계에 처음에 비동기명령을 시동할 때와 명령이 완료했을 때를 알려주기 위한 완료 발송자(completion dispatcher)에 콜백함수를 등록하기 위해 proactor 패턴을 사용할 수 있다. 운영체계는 이때 웹서버입장에서 명령을 수행하며 순차적으로 운영체계 내의 잘 알려진 곳에 결과를 적재(queue)한다. 완료 발송자(Completion Dispatcher)는 완료 알림메세지들을 뽑아내고(dequeue), 어플리케이션 동작위주의 웹서버 코드를 담은 알맞은 콜백함수를 실행하는 역할을 담당한다.


그림 5와 6은 proactor 패턴방식의 이벤트 디스패칭을 사용하여 디자인된 웹서버가 한개 이상의 쓰레드내에서 여러 클라이언트들을 어떻게 동시처리하는지를 보여준다.

Figure 5. 클라이언트가 Proactive 방식의 웹서버에 접속


그림 5는 클라이언트가 웹서버로 접속했을때 실행되는 단계의 순서를 보여준다.

1. 웹서버는 acceptor에게 비동기 accept 처리를 초기화하도록 알려준다.
2. acceptor는 운영체계의 기능을 이용하여 비동기 accept 요청을 초기화하고, 그 자신을 완료 핸들러(Completion Handler)와 완료 발송자(Completion Dispatcher)의 참조로써 넘기게 된다. (이것은 비동기 accept의 완료여부를 acceptor에게 알려주는데 사용된다.)
3. 웹서버는 완료 발송자의 이벤트 루프를 실행한다.
4. 클라이언트가 웹서버에 접속한다.
5. 비동기 accept 명령이 완료하면, 운영체계는 완료 발송자에게 통지한다.
6. 완료 발송자는 acceptor에게 통지한다.
7. acceptor는 HTTP 핸들러를 생성한다.
8. HTTP 핸들러는 클라이언트로 부터 전송되는 요청 데이타를 비동기적으로 읽는 작업을 초기화하고 그 자신을 완료 핸들러(Completion Handler)와 완료 발송자(Completion Dispatcher)의 참조로써 넘기게 된다. (이것은 비동기 읽기작업의 완료여부를 acceptor에게 알려주는데 사용된다.)

Figure 6. 클라이언트가 Proactive 방식의 웹서버에 요청을 보냄


그림 6은 proactor 패턴을 적용한 웹서버가 HTTP GET 요청을 서비스하기위한 단계를 보여준다. 이 단계는 아래와 같다.


1. 클라이언트가 HTTP GET 요청을 전송한다.
2. 읽기 작업이 완료되면 운영체계는 완료 발송자에게 통지한다.
3. 완료 발송자는 HTTP 핸들러에게 통지한다. (2단계와 3단계는 전체 요청 메세지가 모두 전송받아질 때까지 반복하게 된다.)
4. HTTP 핸들러는 요청 데이타를 파싱한다.
5. HTTP 핸들러가 동기적으로 요청된 화일을 읽어들인다.
6. HTTP 핸들러는 화일 데이타를 접속된 클라이언트로 전송하기위한 비동기 명령을 초기화한다. 그리고 그 자신을 완료 핸들러(Completion Handler)와 완료 발송자(Completion Dispatcher)의 참조로써 넘기게 된다. 이것은 비동기 화일전송작업의 완료여부를 HTTP 핸들러에게 통지하는데 사용된다.
7. 전송작업이 완료되면 운영체계는 완료 발송자에게 통지한다.
8. 이때 완료 발송자는 완료 핸들러에게 통지한다. (6~8단계는 화일이 모두 전송될때까지 반복된다.)


웹서버에 proactor 이벤트 디스페칭 모델을 적용한 C++ 코드예제가 8장에 소개되어있다. proactor 패턴을 적용했을 때 가장 큰 잇점은 다중으로 실행되는 동시처리 명령들을 꼭 여러개의 쓰레드를 필요로 하지않으면서 병렬적으로 시동하고 실행할 수 있다는 점이다. 각 명령들은 비동기적으로 어플리케이션에 의해 시동되며, 운영체계의 입출력 부속시스템내에서 완료될 때 까지 실행을 계속한다. 이제 명령들을 초기화하는 쓰레드는 한가지 작업만을 전담하지 않고 추가된 요청들을 서비스해주는 것이 가능하다. 예를 들자면, 앞의 예제에서 완료 발송자는 단일 쓰레드 방식이 될 수 있는 것이다. HTTP 요청이 서버에 도착하면, 단일 완료발송자 쓰레드는 요청 메세지를 파싱하고, 화일을 읽고, 클라이언트에게 요청에 대한 응답을 전송한다. 응답이 비동기적으로 보내어지기 때문에, 다수의 응답을 동시에 부분적으로 처리할 수 있게 되는 것이다. 게다가, 동기적 화일읽기 작업은 비동기 화일읽기 작업으로 교체할 수도 있을 것이다. (이러면 동시처리될 가능성이 더 높아지게된다.) 만약 화일읽기작업이 비동기적으로 수행된다면, HTTP 핸들러에 의해 처리되는 단 하나의 동기적 작업은 HTTP 프로토콜 파싱밖에 없게 된다.


proactor 모델의 주요 단점은 reactor 모델보다 프로그래밍 로직이 보다 더 복잡해질 수 있다는 것 이다. 게다가, 비동기 명령들은 가끔 예측하기 힘들고 반복되지않는 실행순서를 가지는 까닭에 proactor 패턴은 실행 분석과 디버그하기가 다소 어렵다. 7장은 비동기 어플리케이션을 단순화시켜주는 (비동기 완료 토큰[8]과 같은) 다른 패턴들을 적용시키는 방법에 대해 설명하고 있다.

적용해야 할 경우

proactor 패턴은 다음과 같은 조건을 한개 이상 만족할 때 사용하기를 권장한다.

  • 호출되는 쓰레드를 블록하지 않고 한개 이상의 비동기 명령들을 실행할 필요가 있을 때.
  • 비동기 명령들이 완료될 때를 통지받아야 할 때.
  • 입출력 모델에 독립적으로 다양한 동시처리 전략이 요구될 때.
  • 어플리케이션 독립적으로 구현된 하부구조로부터 어플리케이션 의존적인 로직을 흡수할 경우 잇점이 많을 때.
  • 멀티쓰레딩 방식 혹은 reactor 디스패칭 방식으로는 성능이 기대한 것보다 낮거나 비효율적인 경우.

구조와 구성요소들

proactor 패턴의 구조는 figure 7에 OMT 표기법으로 그려져있다.

Figure 7. Proactor 패턴의 구성요소들


proactor 패턴의 핵심 구성요소는 다음과 같다:

  • Proactive Initiator (웹서버 어플리케이션의 주 쓰레드) : 어플리케이션 요소중의 하나이며 비동기 명령을 초기화 한다. Proactive Initiator는 완료 핸들러를 완료 발송자에게 등록한다. 완료 디스패쳐는 명령이 완료되었을때 통보해준다.
  • 완료(Completion) 핸들러 (the Acceptor and HTTP Handler): Proactor패턴에서는 완료 핸들러를 비동기 명령이 완료되었을때 어플리케이션으로 통보해주기위한 인터페이스로 사용한다.
  • 비동기 명령 (the methods Async Read, Async Write, and Async Accept) : 비동기 명령은 어플리케이션의 입장에서 요청을 실행하기 위한 용도로 사용된다. 어플리케이션이 비동기 명령을 호출하면 해당 명령은 어플리케이션의 스레드 컨트롤을 가져오지 않고 수행된다.(이에 반해 reactive 이벤트 분배 모델은 명령은 동기적으로 수행하기위해 어플리케이션의 스레드 컨트롤을 빼앗아온다) 따라서, 어플리케이션의 관점에서 보면 명령은 비동기적으로 수행된다. 비동기 명령이 완료되면 비동기 명령 프로세서는 어플리케이션에게 통보하는 일을 완료 분배자에게 위임한다.
  • 비동기 명령 프로세서 (the Operating System) : 비동기 명령 프로세서는 비동기 명령을 실행한다(명령이 완료될때까지). 이 컴포넌트는 일반적으로 OS에 의해 구현된다.
  • 완료(Completion) 분배자 (the Notification Queue) : 완료 분배자는 비동기 명령이 완료되었을때 어플리케이션의 완료 핸들러를 호출해준다. 비동기 명령 프로세서가 비동기적으로 초기화 명령을 완료했을때도 완료 분배자는 어플리케이션의 입장에서 콜백을 수행해준다.

Collaborations

모든 비동기 명령에 대해 몇개의 잘 디자인된 단계가 있다. 높은 수준의 추상화 레벨에서 어플리케이션은 비동기적으로 명령을 초기화하고 명령이 완료되었을때 통보받는다. Figure 8은 패턴 구성요소들 사이에 반드시 일어나야하는 다음 상호작용들을 보여준다.

Figure 8. Proactor패턴을 위한 상호작용 다이어그램


1. Proactive 초기자는 명령을 초기화 : 비동기 명령을 수행하기 위해서 어플리케이션은 비동기 명령 프로세서에서 명령을 초기화 해야한다. 예를 들면 웹서버는 OS에게 특정한 소켓을 이용하여 파일을 네트워크 너머로 전송하겠다고 요청할수도 있다. 그런 명령을 요청하기 위해서는 웹서버는 반드시 사용하고자하는 파일과 네트워크 커넥션을 명시해야한다. 더욱이, 웹서버는 파일 전송이 완료되었을때 어떤 완료 핸들러로 통보를 받을지, 어떤 완료 분배자가 콜백을 수행해줄지도 명시해야한다.
2. 완료 명령 프로세서는 명령을 수행 : 어플리케이션이 명령을 호출하면 비동기 명령 프로세서는 비동기적으로 명령을 수행해준다. 현대 OS(솔라리스나 윈도우 NT 같은)는 커널단에서 비동기 IO 서브시스템을 제공해준다.
3. 비동기 명령 프로세서는 완료 분배자에게 완료를 통보 : 명령이 완료되었을 때, 비동기 명령 프로세서는 명령이 초기화될때 명시되었던 완료 핸들러와 완료 분배자를 찾아낸다. 그리고 비동기 명령의 결과와 콜백을 위한 완료 핸들러를 완료 분배자에게 넘겨준다. 예를 들어 비동기 파일 전송이 완료되었을때 명령의 결과(성공인지 실패인지)와 몇바이트가 전송되었는지를 알려준다.
4. 완료 분배자는 어플리케이션에게 완료를 통보 : 완료 분배자는 결과 데이터를 어플리케이션에게 알려주기 위해 완료 핸들러를 호출한다. 예를 들어 비동기 읽기 명령이 완료되면 완료 핸들러는 일반적으로 새로 도착한 데이터(방금 읽어낸 데이터)의 포인터를 넘겨준다.

결론

이 장은 Proactor 패턴의 장단점을 설명한다.

장점

proactor 패턴은 다음과 같은 장점들을 가지고 있다:

  • 고려사항에 대한 구분이 보다 더 명확함: Proactor 패턴은 어플리케이션과는 독립적인 비동기 체계들을 어플리케이션 고유의 기능과 분리시켜준다. 어플리케이션에 독립적인 메커니즘들(비동기 명령과 관련된 완료 이벤트들을 어떻게 디멀티플렉싱하는가, 완료 핸들러에 정의된 콜백 함수를 어떻게 분배하는가)은 재사용이 가능한 컴포넌트가 된다. 마찬가지로 어플리케이션에 명시된 기능들은 특정한 서비스들을 수행한다(HTTP처리와 같은).
  • 어플리케이션 로직의 이식성 증가 : 이벤트 디멜티플렉싱을 수행하는 OS의 호출로부터 독립적인 인터페이스는 어플리케이션의 이식성을 향상시켜준다. 이런 시스템 호출들은 여러 이벤트로부터 동시에 발생할수도 있다. 이벤트 소스는 IO포트, 타이머, 동기화 객체, 시그널 등을 포함할 수도 있다. 실시간 POSIX 플랫폼에서 비동기 함수들은 aio family of API들로 부터 제공된다. 윈도우즈 NT에서는 IO completion port나 overlapped IO로 비동기 IO를 구현한다.
  • 완료 분배자가 동시처리 체계를 은폐(encapsulate)시켜준다: 완료 분배자를 비동기 명령 프로세서로부터 분리시키는 장점중의 하나는 어플리케이션이 다른 구성요소들에게 영향을 미치지 않으면서도 완료 분배자를 다양한 동시처리 전략으로 설정할수 있다는 점이다. 섹션 7에서 논의된 바와 같이 완료 분배자는 싱글스레드 혹은 스레드 풀과 같이 다양한 동시처리 전략으로 설정이 가능하다.
  • 쓰레딩 정책이 동시처리 정책과 분리된다.: 비동기 명령 프로세서가 오래 걸릴수도 있는 명령들을 Proactive Initiator를 대신하여 처리해주기 때문에 어플리케이션은 굳이 스레드를 여러개 생성하지 않아도 된다. 이것은 어플리케이션이 스레딩 정책을 독립적으로 설정할수 있게 해준다. 예를 들어 웹서버는 하나의 CPU에 하나의 스레드만을 사용할수도 있다. 하지만 많은 수의 브라우저들의 끊임없는 요청을 처리해주기 위해서는 더 많은 스레드가 필요할 수도 있다.
  • 효율의 증가: 멀티스레드 OS는 여러 스레드사이에 컨텍스트 스위칭을 반복수행한다. 이런 작업중에 OS가 놀고있는 스레드에 컨텍스트 스위칭을 한다면 전체 어플리케이션의 성능은 두드러지게 떨어질수 있다. 예를 들어 비효율적인 스레드에 완료를 poll 한다거나. Proactor 패턴은이벤트 진행의 컨트롤이 가능한 스레드만 활성화 시키기 때문에 이런 컨텍스트 스위칭 비용이 들지 않는다. 예를 들면 웹서버는 현재 pending 되어있는 GET 요청이 없다면 HTTP 핸들러를 활성화 시키지 않는다.
  • 어플리케이션 동기화의 단순화 : 완료 핸들러 자체에서 스레드를 생성하지 않는 한, 어플리케이션 로직은 동기화 이슈에 대한 고려없이 작성되어도 된다. 완료 핸들러는 마치 전통적인 싱글 스레드 환경에 있는 것 처럼 작성될수도 있다. 예를 들면 웹서버의 HTTP Get 핸들러는 비동기 읽기 명령(윈도우즈의 TransmitFile 함수와 같은)을 통해 디스크를 액세스 할 수 있다.

단점

proactor 패턴은 다음과 같은 단점들을 가지고 있다:

1. 디버그하기 어렵다 : proactor 패턴을 사용하여 개발된 어플리케이션은 디버그하기가 어려워질 수 있다. (이것은 뒤집어진 제어 흐름이 프레임워크 하부구조와 어플리케이션에서 정의된 핸들러상의 콜백사이를 왔다갔다하기 때문이다. 한마디로 추적하다보면 내가 어디있지? 하는 부분을 말한다.) 이 때문에 프레임워크의 실행과정 중에 "한줄한줄씩 밟아나가는 방식"의 디버그는 정말로 어려워질 수 있다. (어플리케이션 개발자들은 프레임워크의 소스 코드를 가지고 있지 않거나 이해하지 못하기 때문이다) 이것은 LEX나 YACC으로 쓰여진 컴파일러의 구문 분석기나 파서를 디버그할 때 부딛히는 문제와 비슷하다. 이런 형태의 어플리케이션에서는, 제어되는 쓰레드가 사용자 정의된 액션 루틴상에 있을때 디버깅은 수월해질 수 있다. (In these applications, debugging is straightforward when the thread of control is within the user-defined action routines.) 한번 제어 쓰레드가 유한결정오토마타(DFA : Deterministic Finite Automata)를 생성하고 반환되면, 어찌되었던간에 프로그램 로직을 따라가는 것은 힘들어진다.


2. 두드러진(outstanding) 명령처리와 스케줄링 : Proactive Initiators는 비동기 명령들이 실행되는 순서를 조정할 수 없을 수도 있다. 그러므로, 비동기 명령 프로세서는 비동기 명령들의 우선순위지정과 취소기능을 지원하도록 주의깊게 제작되어야 한다.

구현

Proactor 패턴은 다양한 방법으로 구현될 수 있다. 이 장은 Proactor 패턴을 구현하는데 필요한 단계를 알아본다.

비동기 명령 프로세서의 구현

Proactor 패턴을 구현하는 첫번째 단계는 비동기 명령 프로세서는 만드는 일이다. 비동기 명령 프로세서는 어플리케이션을 대신하여 명령을 비동기적으로 수행해주는 역할을 한다. 이것의 결과로 비동기 명령 API들을 정의하는 일과 비동기 명령 엔진을 만드는 두 가지 해야 할 일이 생긴다.

비동기 명령 API를 정의하기

비동기 명령 프로세서는 반드시 비동기 명령을 요청할 수 있는 API들을 제공해야 한다. 아래에 API들을 디자인하는데 고려해야 할 사항이 있다.

  • 이식성: API는 어플리케이션이나 Proactor Initiator에 종속되어서는 안된다.
  • 융통성: 종종 비동기 API는 많은 타입의 명령에 공유된다. 예를 들면 비동기 IO 명령은 다중 IO작업을 수행하기 위한 방편으로 사용될수도 있다(네트워크나 파일 같은). 이런 재사용을 지원하도록 API를 디자인하는 것이 유익할 수 있다.
  • 콜백 : Proactor Initiator는 반드시 명령이 호출될때 콜백을 등록해야한다. 콜백을 구현하기위한 일반적인 방법은 calling object나 caller라고 불리는 인터페이스를 export하는 방법이 있다. Proactor Initiator는 반드시 비동기 명령 프로세서에게 명령이 완료되면 호출될 완료 핸들러를 알려야한다.
  • 완료 디스패쳐 : 어플리케이션이 다중 완료 분배자를 사용할 수도 있기 때문에Proactor Initiator 또한 어떤 완료 분배자가 콜백을 수행해 줄 것인지 명시해야 한다.

    이런 사항들을 감안한, 비동기 읽기와 쓰기를 제공하는 API가 있다. Asynch_Stream클래스는 비동기 읽기와 쓰기를 초기화 하는 팩토리이다. 한번 생성되면 여러개의 읽기와 쓰기 작업이 이 클래스를 이용하여 시작될 수 있다. 비동기 읽기 작업이 완료되면Asynch_Stream::Read_Result 클래스가 완료 핸들러에 있는 handle_read 함수를 통해 전달될 것이다. 마찬가지로 비동기 쓰기 작업이 완료되면Asynch_Stream::Write_Result 클래스가 완료 핸들러에 있는 handle_write 함수를 통해 전달될 것이다.

비동기 명령 엔진 구현하기

비동기 명령 프로세서는 반드시 명령을 비동기 방식으로 수행하는 메커니즘을 포함해야한다. 다르게 설명하면, 어플리케이션 스레드가 비동기 명령을 수행하면 이 명령은 어플리케이션의 스레드 컨트롤을 가져오지 않고 수행되어야 한다(블로킹되지 않고 바로 리턴되어야 한다는 의미). 다행히도 현대의 OS들은 비동기 명령을 위한 메커니즘을 제공한다(예를 들면 POSIX 비동기 I/O나 Windows NT의 overlapped I/O). 이 경우에는 이 패턴을 구현하는 부분은 단순히 플랫폼의 API들과 위에 서술된 비동기 명령들과 매핑만 시켜주면 된다. 하지만 만일 OS가 비동기 명령을 지원하지 않는다면 비동기 명령엔진을 구현하는 몇가지 테크닉이 있다. 가장 직관적인 해결법은 전용 스레드에서 어플리케이션의 요청한 비동기 명령을 수행하는 방법일 것이다. 스레드된 비동기 명령을 구현하기 위해서는 몇가지 단계가 있다.

알려진 사용처

아래는 Proactor 패턴을 사용했다고 알려진 곳들이다.

  • I/O Completion Ports in Windows NT : Windows NT는 Proactor 패턴을 구현하고 있다. 새로운 네트워크 커넥션을 받는 명령, 파일이나 소켓을 읽거나 쓰는 명령, 네트워크 커넥션을 통해 파일을 전송하는 명령이 Windows NT에 의해 제공된다. OS가 바로 비동기 명령 프로세서 이다. 명령의 결과는 I/O completion port에 쌓인다(이것이 완료 분배자의 역할을 수행).
  • The Unix AIO Family of Asynchronous I/O Operations : 실시간 POSIX 플랫폼에서는 Proactor패턴이 aio family of APIS로 구현되어있다. 이런 OS의 기능들은 위에 기술된 Windows NT와 상당히 유사하다. 한가지 다른 점이 있다면 UNIX 시그널은 진짜 비동기 완료 분배자로 구현되어있다.(Windows NT의 API는 진짜 비동기 방식은 아니다)
  • ACE Proactor : ACE의 비동기 컴포넌트들은 Windows NT의 I/O Completion Ports와 UNIX 플랫폼의 aio APIs를 캡슐화한다. ACE Proactor의 추상화는 Windows NT의 표준 C APIs를 통해 객체지향 인터페이스를 제공한다. 소스코드는 ACE 웹사이트에서 얻을수 있다(www.cs.wustl.edu/_schmidt/ACE.html).
  • Asynchronous Procedure Calls in Windows NT : 어떤 시스템(Windows NT와 같은)은 비동기 프로시져 콜(APC)을 지원한다. APC는 함수는 비동기적으로 특정한 스레드에서 실행되게 해준다. APC가 스레드에 queue되면 시스템은 소프트웨어 인터럽트를 실행한다. 다음번에 스레드가 schedule되면 APC에서 실행된다. OS에 의해 만들어지는 것을 커널모드 APC라고 하고 어플리케이션에 의해 만들어지는 것을 유저모드 APC라고 한다.

관련이 있는 패턴들

Figure 9는 Proactor와 관련이 있는 패턴들을 보여주고 있다.

Figure 9. Proactor패턴과 연관된 패턴들


The Asynchronous Completion Token (ACT) 패턴은 일반적으로 Proactor 패턴과 결함되어 쓰인다. 비동기 명령이 완료되었을때, 어플리케이션은 이벤트를 적절하게 다루기 위해 단순히 통보만 받는 것보다 더 많은 정보가 필요할 수도 있다. The Asynchronous Completion Token 패턴은 어플리케이션이 비동기 동작 완료와 관련있는 정보를 효율적으로 알수 있게 해준다.