Tuesday, February 16, 2010

[C++] Generic Callback Functor

정말 오래간만에 내가 프로그래머라는 걸 알리는 글. :-)

이번에 IOCP 를 이용한 비동기 Callback routine 을 짜다가,
boost 의 function 과 bind 를 활용한 generic callback functor 를 만들어 보았다.
물론, 내가 스스로 100% 완전 개발한 건 아니고, ㅎㅎ
아이디어는 공부하다가 발견한 부분에서 채택한 후 살짝 변경을 한 것이다.

이 generic 한 callback functor 의 장점은

- iocp 자체는 callback 함수가 불려야 할 본체 instance 의 포인터를 전혀 몰라도 된다.
- boost::bind + function 이 std::mem_fun 과 다르게 동작하는 점이 위의 본체 instance 가 살아있던 지워지던 신경을 쓰지 않는다. 그냥 함수포인터만 유효하면 callback 함수가 불리게 된다.
- 물론, 본체 instance 가 이미 지워져서 유효하지 않을 때는 this 의 그 어떤 걸 사용해도 안되게 되지만, 지워지지 않은 상황이라면 this 의 모든 access 가 가능하다.
- iocp 자체의 소스코드는 매우 간결하게 끝난다.
- worker thread 에서 수행되어야 할 부분의 소스코드가 원래 class 에 들어가게 되므로 불필요한 include 를 피할 수 있다.
- 코드가 일관성 있게 되기 때문에 찾아서 고치기가 수월해 진다.


물론 단점도 있다.

- boost::bind + function 을 쓰기 때문에 c style callback 이나 std::mem_fun 보다 당연히 느릴 수 밖에 없다.
(속도 비교 : http://www.dogfootlife.com/archives/108)
- 그러므로, 미리 등록해서 쓸 수 있는 callback 의 경우에는 c style 이나 std::mem_fun 을 추천한다.
- template instance 할 때 코드가 길어질 수 밖에 없다. typedef 로 최대한 줄이는 걸 추천.


먼저 worker base 부분.

class IOWorkerBase { public: IOWorkerBase() {;} virtual ~IOWorkerBase() {;} virtual void Execute() {;} };

Base class 를 만든 이유는 iocp worker thread 안에서 template 인스턴스를 몰라도 Execute 함수를 호출하기 위해서임.
다른 이유는 일절 없음. :)

이번엔, Worker class.

template<typename context_type> class IOWorker : public IOWorkerBase { public: typedef IOWorker<context_type> tSelfType; typedef boost::function<void (tselftype&)> tCallbackFunctor; IOWorker(const context_type context, const tCallbackFunctor& functor) : m_context(context) , m_callback( functor ) {} IOWorker(const tCallbackFunctor& functor)  : m_callback( functor ) {} virtual ~IOWorker() {;} inline void SetContext(const context_type& context) { m_context = context; } inline context_type& GetContext() { return m_context; } inline const context_type& GetContext() const { return m_context; } inline const tCallbackFunctor GetFunctor() const { return m_callback; } virtual void Execute() { m_callback(*this); } private: context_type m_context; tCallbackFunctor m_callback; };
위의 functor 는 new 와 delete 를 프로그래머에게 맡겨버리는 코드이다.
main / worker thread 의 관계에 따라서 new / delete 부분을 functor 에 포함시킬 수 있다. 그 부분은 글의 마지막 부분에 다루기로 한다.

callback 함수 구현 부분.
간단한 예를 들기 위해서 std::string 을 이용했다.

void eeodl::IOCallback(IOWorker<std::string>& worker) { std::cout << "Context : " << worker.GetContext() << std::endl; }

functor 생성 부분.
생성 후 iocp 에 포스트 한다.

std::string text = "eeodl's generic io callback routine"; IOWorker<std::string>* pWorker = new IOWorker<std::string> (boost::bind(&eeodl::IOCallback, &eeodl, _1)); pWorker->SetContext(text); iocp.post(pWorker);
위의 bind 안의 두번째 인자 eeodl 은 eeodl instance(!) 의 포인터를 넘겨줘야 함.
하지만 위에서도 얘기했듯이 지워지더라도 callback 은 수행이되고, 내부를 access 하지 않는 이상 crash 나지 않음.

iocp worker thread 부분.

IOWorkerBase* pBase = NULL; GQCS(...); // GQCS 의 인자에 pBase 를 넣어준다. // GQCS 결과 에러체크. 실패 처리는 적절히. if (pBase) pBase->Execute();
끝.

이 예제에서 주의할 점은 context 로 std::string 을 사용하였는데, 이는 클래스가 복사가 되므로 문제가 생기지 않는다.
하지만, 자신이 따로 메모리 할당한 context 를 사용하고 싶을 때는 memory 관리를 직접해주어야 하는데, smart pointer 를 이용하면 약간의 번거로움을 줄일 수 있다.
이 때 boost::shared_ptr 를 thread safe 하기 때문에 안전하게 쓸 수 있지만, 역시 문제는 느리다는 거.

참고로 위의 worker 는 메모리 관리를 따로 하지 않지만 Execute function 을 아래와 같이 해주면 쉽게 메모리 관리를 할 수 있다.

virtual void Execute() { m_callback(*this); delete this; }
물론 main - worker thread 관계에 따라 memory 관리를 직접하고 싶어하는 경우가 많이 생기기 때문에 위의 Execute 는 특별한 경우라 할 수 있겠다.

4 comments:

  1. 악! 다 쓰고 보니까 template 부등호 부분 다 깨져서 아예 나오지를 않는구나. -_-;
    고칠까 말까 고칠까 말까. 악 번거로워.

    ReplyDelete
  2. 헉헉 20분 동안 다 고쳤다.
    아 번거로워. 구글 이놈들 제발 이 기능좀 고쳐줘 ㅠㅠ

    ReplyDelete
  3. 저도 저번 주에 callback 구현한다고 몇 시간 삽질했는데 추상화를 위해서는 boost::bind 말고는 대안이 없더군요. 그런데 STL은 다 좋은데 아무래도 쌩으로 하는 것보다는 성능이 좋지 않아서 아직까지 성능에 눈에 불을 밝히는 서버 입장에서는 마음 놓고 사용하기가 좀 그렇네요^^;

    ReplyDelete
  4. jacking// 네 글쵸. 저도 bind 쓰다가도 속도 때문에, 미리 등록할 수 있는 경우에는 그냥 c-style callback 쓰고 있지요. boost 는 확실히 편한 코딩은 되지만, 더 나은 성능과는 거리가 있는 듯 해요.

    ReplyDelete