2016년 10월 24일 월요일

boost asio에서 iocp 버전 코드 분석

boost asio에서 iocp를 이용한 비동기 작업처리를 어떻게 구현하고 있는지를 직접 구현하면서 되짚어 보려한다.

1. start_post() - 비동기 호출 인터페이스

 먼저 비동기 작업이 완료되면 호출되는 함수 콜백과 콜백함수 호출에서 참조되는 정보를 Context라는 클래스에 정의할 때 비동기 작업의 시작은 start_post() 함수 호출로부터 시작된다.
class Context { ... };
void callback(std::shared_ptr<Context>);

void start_post() {
  std::shared_ptr<Context> c(new Context);
  // c에 콜백에 필요한 정보 초기화
  ...
  io_service.post(std::bind(callback, c));
}
 여기서 Context는 단순히 POD가 아니라 포인터 객체, stl container등 다양한 정보를 담고 있을 수 있다. 물론 이 Context객체는 콜백이 호출이 완료되는 시점까지 lifetime이 지속되어야 한다.
 참고로 boost asio는 위와 같은 폼으로 시작한 비동기 작업이 완료되면 io_service.run() 함수가 호출되었던 쓰레드에서 해당 콜백이 호출된다.
 io_service.post()는 bind를 사용한 함수객체를 인자로 받고 있는데 이는 즉, 고정된 인터페이스가 아닌 임의의 타입의 콜백을 등록할 수 있다는 의미이며 이는 템플릿을 통해 구현한다. 또한 함수객체는 임시객체로 만들고 있으므로 이를 콜백이 호출될 때까지 lifetime을 연장시키는 부분도 구현해야 한다.

2. io_service::post()

template <typename Handler>
void io_service::post(Handler && handler) {
  Handler h(std::move(handler));
  impl.post(h);
  // 위 코드 대신 impl.post(handler);를 써도 된다
}
 먼저 bind로 만들어진 임시 함수 객체를 인자로 받기에 io_service::post()는
 1. 함수 템플릿
 2. 템플릿 파라미터는 함수객체 타입인 Handler
 3. 함수 인자로는 Handler 타입의 rvalue 참조 타입
으로 정의한다.
 boost asio는 각 플랫폼 별로 별도의 io_service구현체(멤버 impl, win_iocp_io_service타입)를 두고 있기에 handler객체를 구현체에게 넘겨주어야 한다. (로컬 변수 h는 실제 boost asio구현에서는 async_result_init라는 클래스 타입의 변수이다. 여기서는 별 의미없는 코드로 아래 주석의 코드로 대체 가능하다.)
 여기서 포커스는 handler객체의 리소스 소유권을 넘겨받고 impl.post() 내에서도 인자로 넘겨진 handler 객체의 리소스 소유권을 가져가는데 있다.

3. win_iocp_io_service::post()

template <typename Handler>
void win_iocp_io_service::post(Handler & handler) {
  typedef completion_handler<Handler> op;
  op::ptr p = { malloc(sizeof(op)), 0 };
  p.p = new (p.v) op(handler);

  enqueue_worker(p.p);
  p.v = p.p = 0; //ptr소멸자에서 reset()때 메모리 free되는것 방지
}
 이 함수는 인자로 받는 handler를 rvalue 참조가 아닌 lvalue 참조로 받았다. 무엇이 되었든지 우리는 이 객체의 소유권을 가져올 것이다. 그런데 iocp queue에 데이터를 넘겨주고 받을 길은 OVERLAPPED 구조체를 통한 길 뿐이다. 그래서 Handler타입의 객체를 scope를 벗어나도 소멸자의 영향을 받지 않으면서 객체의 모든 정보를 담을 수 있는 방법을 쓴다.

 1. Handler 타입 크기만큼의 메모리를 할당하고
 2. 할당된 메모리를 Handler타입의 객체인 것처럼 사용하여 handler의 모든 리소스를 새로 할당한 메모리 객체로 옮겨온다. (in-place new operator, rvalue ref. constructor)
 3. enqueue_worker()함수는 새로 할당된 메모리의 데이터를 가지고 실제 비동기 작업을 수행하기 위해 작업큐에 등록

 이제 인자의 handler객체의 리소스 소유권은 op::ptr 타입 객체의 멤버 p가 가리키는 새로운 Handler 객체로 넘어갔으며 p는 실제로는 포인터이므로 함수를 벗어나도 Handler 소멸자에 의해 리소스가 영향을 받지 않게 된다. (물론 이 방식으로는 콜백이 호출이 끝나면 명시적 소멸자를 호출해주어야 한다. 이후 자세히 설명)

4. OVERLAPPED, win_iocp_operation, completion_handler<Handler>

LPOVERLAPPED overlapped;
GetQueuedCompletionStatus(..., &overlapped, ...);

// overlapped를 어떤 형태로 변형하고 이 변형된 타입이 handler 콜백 호출
 앞에서 언급한대로 iocp에서 비동기작업에 필요한 정보를 전달할 방법은 OVERLAPPED 구조체가 뿐이다. 또한 작업이 완료되면 위 코드에서처럼 OVERLAPPED 구조체 포인터로 post()시점에 넘겨준 정보를 받고 이를 가지고 어딘가 있을 handler 콜백을 호출해야 한다.
 결론만 얘기하면 overlapped 포인터를 비템플릿 타입인 win_iocp_operation 타입으로 캐스팅하고 win_iocp_operation의 멤버함수를 호출함으로써 적절한 handler를 호출되도록 OVERLAPPED구조체로부터 상속한 클래스를 만든다.


class win_iocp_operation : public OVERLAPPED {
public:
  void complete() { func(this); }
protected:
  typedef void(*func_type)(win_iocp_operation*);
  win_iocp_operation(func_type f) : func(f) { }
private:
  func_type func;
};
func_type타입의 함수 포인터를 인자로 받는 클래스를 OVERLAPPED 구조체로부터 상속한다. 그러면 앞에서 overlapped포인트는 다음과 같이 쓸 수 있다.

win_iocp_operation * op = static_cast<win_iocp_operation*>(overlapped);
op->complete();
그리고
template <typename Handler>
class completion_handler : public win_iocp_operation {
public:
  completion_handler(Handler & h)
    : win_iocp_operation(&completion_handler::do_complete)
    , handler(std::move(h))
  {}
  static void do_complete(win_iocp_operation * op) {
    completion_handler * h(static_cast<completion_handler*>(op));
    // p는 그냥 로컬변수쯤으로 여기자. 자세한 설명은 뒤에
    ptr p = { h, h };
    Handler handler(std::move(h->handler));
    // h가 가지고 있던 모든 리소스를 handler로 넘기고 p,v메모리를 해제한다.
    // 이유는 아래 콜백handler()가 예외를 던질때를 대비해서 미리 해제를 하는듯하다
    p.reset();
    handler();
  }
private:
  Handler handler;
};
 completion_handler클래스는 Handler타입의 핸들러를 인자로 받는 클래스이다. 이로 보아 win_iocp_operation::complete()가 호출될 때 completion_handler<Handler>::do_complete()가 호출됨을 알 수 있다. 이렇게 함으로써 콜백 호출시점엔 직접적으로 Handler타입을 몰라도 올바른 타입의 handler가 호출될 수 있는 구조가 만들어진다.

 win_iocp_operation과 completion_handler<Handler>의 관계는 한마디로 요약하면 completion_handler<Handler>는 OVERLAPPED+콜백정보를 만들때, win_iocp_operation은 작업이 완료되고 콜백이 호출될 때에만 사용되는 것으로 용도가 구분됨을 알 수 있다.

5. struct completion_handler<Handler>::ptr

 위에 compltion_handler<Handler>::ptr 구조체는 헬퍼 클래스로 completion_handler 내부 클래스로 다음과 같은 모양이다.
struct ptr {
  void * v;
  completion_handler * p;
  ~ptr() { reset(); }
  void reset() {
    if (p) { p->~completion_handler(); p = 0; }
    if (v) { free(v); v = 0; }
  }
};
 ptr 타입 변수 p,v에 할당된 메모리를 ptr가 소멸될 때 적절히 해제해주기 위한 도구 정도로 보면 되겠다.

댓글 없음:

댓글 쓰기