Inter-thread communication in C++ #6

Posted at 2008. 11. 3. 07:00 // in S/W개발/C++ 이야기 // by 김윤수


2008/10/29 - [S/W개발/C++ 이야기] - Inter-thread communication in C++ #5

지난 글에 이어서 ITC 클래스 설계를 계속 수정해 보고 있습니다. 하면 할수록 맘에 들지 않는 부분들이 생겨서 계속 수정하게 되네요. 1) 이번에는 다음 책에서 소개된 바 있는 Policy-based design을 채용하려고 노력해 봤습니다. 

MODERN C++ DESIGN
카테고리 컴퓨터/인터넷
지은이 안드레 알렉산드레스쿠 (인포북, 2003년)
상세보기

결과는 여러분이 좀 판단해 주시구요. 저도 지식이 일천한지라 제가 해놓구서 잘 설계한 것인지 저도 잘 판단이 안 서네요. ㅠ.ㅠ 이럴 때는 고수가 짠하고 나타나서 이런 건 이렇게 이렇게 하면 되지라고 말해주면 좋겠습니다.

Policy-base design을 채용해보려고 했던 가장 큰 이유는 ITCThread와 ITCBusyThread가 대부분의 내용이 동일하고 doIt()과 run()의 내용이 아주 약간만 다르다는 것을 보고는 어떻게 refactoring을 할 수 없을까하고 고민하다가 약간 다른 부분을 Policy로 표현해보자라는 생각으로 연결되었기 때문입니다. 그래서 그냥 ITCThread를 위한 Policy 는 WaitAndExecute라고 이름을 지었고, ITCBusyThread는  ProcessAndExecute라고 이름을 지었습니다. 결과적으로 다음과 같이 수정하였습니다.

namespace itc {

typedef boost::function<void (void)> CallType;
   
struct ProcessingPolicy
{
    virtual void queue(CallType call) = 0;
    virtual void execute() = 0;
};

class WaitAndExecute : public ProcessingPolicy
{
public:
    WaitAndExecute();

    virtual void queue(CallType call);
    virtual void execute();

private:
    boost::mutex m_mutex;
    boost::condition_variable m_cond;
    bool m_req;
    std::queue<CallType>  m_callQ;   
};

class ProcessAndExecute : public ProcessingPolicy
{
public:
    ProcessAndExecute();

    virtual void queue(CallType call);
    virtual void execute();

private:
    virtual void process() = 0;

    boost::mutex m_mutex;
    std::queue<CallType>  m_callQ;
};

template <typename Callee, typename PolicyType = WaitAndExecute>
struct ITCThread
{
    ITCThread(const std::string name = "itcthr") :
        ......, m_policy(new PolicyType), ......
    { ...... }

    void doIt(CallType call)
    {
        m_policy->queue(call);
    }

    ......
private:
    void run()
    {
        std::cout << m_name << ": starting..." << std::endl;
        while (m_run)
        {
            m_policy->execute();
        }
    }
    ......
    boost::shared_ptr<PolicyType> m_policy;
    ......
};

}

보시다시피 ProcessingPolicy는 call queue를 관리하는 로직을 따로 분리한 것이라 보시면 됩니다. ITCThread내에서는  doIt()과 run()에서 각각 ProcessingPolicy의 queue(), execute() 멤버 함수를 호출하고 있습니다.  default ProcessingPolicy는 WaitAndExecute로 좀 더 자주 사용될만한 Policy를 명시했습니다. 이렇게 함으로써 기존에 ITCThread를 쓰던 코드는 거의 변경없이 컴파일이 가능했습니다. 또한 ITCThread와 ITCBusyThread에서 중복됐던 코드는 모두 제거할 수 있게 됐습니다.

한 가지 주의할 점이 있다면 ProcessAndExecute Policy는 뭔가 내부적으로 처리하는 로직을 먼저 실행한 후에 ITC call을 처리하는 정책을 뜻하기 때문에 뭔가 내부적으로 처리하는 로직을 표현하기 위해 private으로 순수 가상 멤버 함수 process()를 추가했습니다. 따라서 ProcessAndExecute Policy를 쓰기 위해서는 먼저 ProcessAndExecute Policy에서 상속을 받은 후 process() 멤버 함수를 반드시 구현해 줘야 합니다.

이렇게 수정하는 와중에 기존의 ITCThreadBase, ITCBusyThreadBase 등의 코드는 모두 ITCThread에 통합해서 itc.h에 작성했고, 대신 WaitAndExecute와 ProcessAndExecute Policy를 itc.cpp에 구현했습니다. 그리고, itcbase.h, itcbase.cpp는 모두 삭제했습니다.

2) 두번째로 생성자 관련 버그를 수정했습니다. 원래는 생성자의 initializer list에서 boost::thread에게 this 를 넘겨 줬었는데, this의 생성자가 완료되기도 전에 thread가 먼저 실행되기 시작해서 초기화되지도 않은 this의 멤버 변수를 참조하려다가 죽어 버리더군요. 역시 생성자에서 this를 함부로 쓰는 걸 위험하다는 걸 다시금 깨달았습니다. 그렇지 않아도 어떤 분이 이 점을 지적해 주셨었는데, 진작 고칠걸하는 후회가 들더군요. ^^

ITCThread의 생성자 코드를 다음과 같이 수정하였습니다.

template <typename Callee, typename PolicyType = WaitAndExecute>
struct ITCThread
{
    ITCThread(const std::string name = "itcthr") :
        m_thr(), m_policy(new PolicyType), m_name(name), m_run(true)
    {
        m_thr = boost::thread(&ITCThread::run, this);
    }
    ......
private:
    ......
    boost::thread m_thr;
    boost::shared_ptr<PolicyType> m_policy;
    std::string m_name;
    bool m_run;
};

보시는 바와 같이 initializer list에서는 m_thr() 와 같이 boost::thread의 기본 생성자가 호출되게 하고(Not-a-thread 객체가 생성됨), 생성자 body에서 boost::thread 임시 객체를 생성하여 m_thr을 다시 초기화하는 방식을 취했습니다. initializer list에서 초기화된 m_thr 은 의미상으로는 Not-a-thread 이기 때문에 아무런 코드가 실행되질 않습니다. 그리고, boost::thread는 MoveAssignable 개념을 지원하기 때문에 boost::thread 임시객체를 생성한 다음에 m_thr에 할당하면 임시객체의 내용이 m_thr로 옮겨가게 됩니다. 이렇게 함으로써 boost::thread 임시 객체가 실행될 때쯤에는 m_policy, m_name, m_run이 모두 제대로 초기화되는 것을 보장할 수 있게 됐습니다. 그전에는 ITCThread가 boost::thread로부터 상속을 받았기 때문에 boost::thread를 먼저 초기화하지 않을 수가 없었지요. 상속이 아닌 Composition을 사용했더니 간단히 문제가 풀리더군요. 지난 번 코드는 쓸데 없이 상속을 쓰는 경우가 많이 있었더군요.

이상의 수정 사항이 담긴 코드를 첨부하니 더 자세히 확인하고 싶으신 분은 참고하세요.



일단 기본적인 골격은 워낙 나름대로 고민을 많이 한 후 도출해낸 결과라서 당분간 그대로 유지할 것 같구요. 추가적인 기능으로 리턴 타입이 있는 함수를 지원해 볼 생각입니다. 리턴 타입이 있는 함수는 아무래도 sync, async 처리를 모두 가능하게 해야할 것 같구요. argument 개수도 세 개까지만 되는 데 아홉개까지도 가능하게 해야하지 않을까 생각하고 있습니다.

추가적으로 있었으면 하는 기능이 있으시면 좀 얘기해 주세요~ 감사합니다.