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 객체의 리소스 소유권을 가져가는데 있다.
1. Handler 타입 크기만큼의 메모리를 할당하고
2. 할당된 메모리를 Handler타입의 객체인 것처럼 사용하여 handler의 모든 리소스를 새로 할당한 메모리 객체로 옮겨온다. (in-place new operator, rvalue ref. constructor)
3. enqueue_worker()함수는 새로 할당된 메모리의 데이터를 가지고 실제 비동기 작업을 수행하기 위해 작업큐에 등록
이제 인자의 handler객체의 리소스 소유권은 op::ptr 타입 객체의 멤버 p가 가리키는 새로운 Handler 객체로 넘어갔으며 p는 실제로는 포인터이므로 함수를 벗어나도 Handler 소멸자에 의해 리소스가 영향을 받지 않게 된다. (물론 이 방식으로는 콜백이 호출이 끝나면 명시적 소멸자를 호출해주어야 한다. 이후 자세히 설명)
여기서 포커스는 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;
};
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가 소멸될 때 적절히 해제해주기 위한 도구 정도로 보면 되겠다.