2012. 12. 5. 10:34

Qt에서 MFC 형태의 Thread/SendMessage/PostMessage 구현 (signal/slot/QThread)

Qt는 전체적으로 윈도우의 MFC와 유사하게 느껴집니다.
Qt와 MFC를 비교하자면, 따로 지면을 할애해야 겠죠.
각자의 장단점이 명확하니깐요. (사실, Qt에 점수를 더 주고 싶군요... MFC도 물론 훌륭한 도구중 하나입니다.)

Qt의 큰 특징중 하나는 Message 처리 매커니즘인데, 그 중심중 하나가 slot과 signal 입니다.
사실, MFC 개발을 하다 Qt로 넘어가면 처음 맞닥들이는 이슈중 하나일 겁니다.
그래서, Qt를 MFC-like하게 만들어 주는 Helper를 공유합니다.

일단, 기본적인 Qt의 UI는 QWidget으로 시작한다는 것만 알아두시고,...

다음과 같이 .h를 진행합니다.

#include "cqthreadhelper.h"
...
class Qxxxxx : public QWidget
{
    Q_OBJECT
...    
public slots:
    void OnMessage(int nIndex, int nValue);
...
signals:
    void WM_MESSAGE(int nIndex, int nValue);
...
private:
    static void THREAD_PROC(void* pContext);
...
private:
    CQThreadHelper m_cThread;
};

즉, 위와 같이 구성됩니다. 눈치를 통해 본다면, WM_MESSAGE가 발생하면 OnMessage로 연결되는거 같습니다. 그리고, THREAD_PROC이 Windows의 _beginthreadex(...)와 유사해 보입니다.
여기서 확인해 볼것이, public slots와 signals인데, 일단 문법은 크게 신경쓰지 않으셔도 됩니다.
다만 알아야 하는 것이 qt의 slot과 signal입니다.

  • signal : MFC(Windows)의 WM_CLOSE와 같은 메시지
  • slot : MFC의 OnClose와 같은 메시지 핸들러

와 같이 비교하면 이해가 쉬울 것입니다. Qt가 MFC와 비교하여 특이한 것은,

  • slot에 return(HRESULT)이 없다. 즉, void 형태이다.
  • slot의 argument가 자유롭다. 즉, LPARAM/WPARAM 대신 int a, int b, char* c, LPST_INFO d, ...
    와 같이 자유롭게 사용할 수 있다.
  • ON_MESSAGE와 같이 signal/slot 등록시 Post형태인지 Send 형태인지 결정해야 한다.
    즉, 등록된 signal은 동일한 형태의 전송방식(Post/Send)을 가진다.
    MFC에서는 예를 들어 WM_CLOSE를 Send/PostMessage가 가능하다.

와 같습니다. 익숙하다 보면 오히려 Windows보다 signal/slot이 좀더 코드 가독성이 띄어나 보일때가 많습니다. return이 없는 단점은 slot의 argument에 return을 보내도록 하면 문제 없겠죠...

이제 다음과 같이 .cpp를 진행합니다.

Qxxxxx::~Qxxxxx()
{
    // 진행중인 thread가 종료될때 까지 기다린다.
    m_cThread.WaitThread();
    delete ui;
}

void Qxxxxx::somefunction(void)
{
    // signal/slot 등록.
    // ON_MESSAGE_SEND와 ON_MESSAGE_POST가 가능함
    ON_MESSAGE_SEND(m_cThread, SIGNAL(WM_MESSAGE(int,int)), SLOT(OnMessage(int,int)));

    // Worker thread 시작
    // 리턴값 체크 요망
    m_cThread.BeginThread(Dialog::THREAD_PROC, this);
}

void Qxxxxx::THREAD_PROC(void* pContext)
{
    int i = 0;

    // Worker thread는 0~5까지 WM_MESSAGE를 Send(emit)한다.
    // 전송시 0~5값과 두배로 더한 값을 두개 전달한다.
    // 그리고 1초 쉰다. (linux에서 sleep은 단위가 초)
// 주의 : emit하면 UI Primary Thread로 호출되지 않을 수 있다.
for (i=0; i<5; i++) { INFOLOG("[THREAD] Try to Signal Emit : %d", i); // emit ((Qxxxxx*)pContext)->WM_MESSAGE(i, i+i);
QMetaObject::invokeMethod((Qxxxxx*)pContext, "WM_MESSAGE", Qt::BlockingQueuedConnection, Q_ARG(int, i), Q_ARG(int, i+1)); sleep(1); } } void Dialog::OnMessage(int nIndex, int nValue) { // 메시지 수신 INFOLOG("[HANDLER] Thread Msg Signal Emitted, i=%d, i+i=%d", nIndex, nValue); // 2초간 대기 sleep(2); }

내용은 주석을 참고하시기 바라며, MFC와 비교해서 크게 다른것은 없을 것으로 보입니다.
물론, ON_MESSAGE 같은 것은, 함수 내에 선언되어야 하며, BeginThread 이전에 등록되어야 합니다.
자세한 것은 cqthreadhelper.cpp / cquthreadhelper.h를 참고하시기 바랍니다.
이제 핵심 Helper 함수를 공유합니다. 사용법은 위에 나온 Skeleton을 참고하시기 바랍니다.
코드는 공유된 040.zip을 참고하시기 바랍니다.

#ifndef CQTHREADHELPER_H
#define CQTHREADHELPER_H

#include <QThread>
#include <stdio.h>
#include <sys/types.h>

#ifndef IN
#define IN
#endif

#ifndef INFOLOG
#define INFOLOG(...) fprintf(stderr, __VA_ARGS__);fprintf(stderr, "\n");
#endif

#ifndef ERRORLOG
#define ERRORLOG INFOLOG
#endif

typedef void (*PFN_THREAD_PROC)(void* pContext);

// UI Thread의 Signal & Slot binding
// cqthreadhelper는 caller의 CQThreadHelper instance를 넘긴다.
// 상속된 QWidget 내부에서 사용한다. (macro에 this를 사용하는데, 그것이 ui로 가정했기 때문)
// ON_MESSAGE_POST : PostMessage(...)처럼 메시지 큐에 넣고 caller(즉, emit)는 바로 리턴된다.
// ON_MESSAGE_SEND : SendMessage(...)처럼 메시지 큐에 넣고 caller(즉, emit)는 해당 slot의 처리 완료까지 대기한다.
#define ON_MESSAGE_POST(cqthreadhelper,signal,slot) cqthreadhelper.connect(this,signal,this,slot,Qt::AutoConnection);
#define ON_MESSAGE_SEND(cqthreadhelper,signal,slot) cqthreadhelper.connect(this,signal,this,slot,Qt::BlockingQueuedConnection);

class CQThreadHelper : public QThread
{
    Q_OBJECT
public:
    explicit CQThreadHelper(QObject *parent = 0);

    ////////////////////////////////////////////
    // 외부 caller는 아래 static 함수로 접근하기 요망
public:
    // Thread Life Time 관련
    bool BeginThread(IN PFN_THREAD_PROC pfnThreadProc, IN void* pContext);
    void WaitThread(void);

protected:
    // 환경 변수
    bool        m_bRunning;
    pthread_t   m_nCallingTID;
    pthread_t   m_nThreadTID;

protected:
    // 실행 변수
    PFN_THREAD_PROC m_pfnThreadProc;
    void*           m_pContext;
    
protected:
    void run();
};

#endif // CQTHREADHELPER_H

#include "cqthreadhelper.h"
#include <PTHREAD.H>

CQThreadHelper::CQThreadHelper(QObject *parent) :
    QThread(parent)
{
    // Thread 동작하지 않음
    m_bRunning      = false;

    // 본 class의 instance를 가지는 thread를 저장
    // 본 class instance 하나를 다른 thread끼리 공유하는 것을 방지하기 위함임.
    // (최대한 동기화 관련 이슈를 원천 봉쇄)
    m_nCallingTID   = pthread_self();

    // 생성된 thread의 tid
    m_nThreadTID    = 0;

    // 실행 변수 초기화
    m_pfnThreadProc = NULL;
    m_pContext      = NULL;
}

void CQThreadHelper::run()
{
    // 시작됨
    m_bRunning = true;

    if (NULL == m_pfnThreadProc)
    {
        ERRORLOG("Invalid Thread Proc");
        goto FINAL;
    }

    // 생성된 thread의 tid를 기록한다.
    m_nThreadTID = pthread_self();

    INFOLOG("Thread(%lu) created", (unsigned long int)m_nThreadTID);
    // Thread Procedure를 호출한다.
    (*m_pfnThreadProc)(m_pContext);

FINAL:
    // 종료됨
    m_bRunning = false;
    INFOLOG("Thread(%lu) finished", (unsigned long int)m_nThreadTID);
}

bool CQThreadHelper::BeginThread(IN PFN_THREAD_PROC pfnThreadProc, IN void* pContext)
{
    bool bRtnValue = false;

    if ((NULL == pfnThreadProc) || (NULL == pContext))
    {
        bRtnValue = false;
        goto FINAL;
    }

    if (0 == pthread_equal(m_nCallingTID, pthread_self()))
    {
        // instance를 생성한 caller thread와 다른 thread에서 호출을 방지한다.
        bRtnValue = false;
        ERRORLOG("Invalid caller thread, %lu, %lu", (unsigned long int)m_nCallingTID, (unsigned long int)pthread_self());
        goto FINAL;
    }

    if (true == m_bRunning)
    {
        bRtnValue = false;
        ERRORLOG("Already Running");
        goto FINAL;
    }

    // 실행 변수 할당
    m_pfnThreadProc = pfnThreadProc;
    m_pContext      = pContext;

    INFOLOG("Try to start thread, parent=%lu", (unsigned long int)m_nCallingTID);

    // 쓰레드를 시작한다.
    // CQThreadHelper::run()이 호출된다.
    // 그리고 아래 함수는 block 함수가 아니다.
    start();

    // 여기까지 왔다면 성공
    bRtnValue = true;

FINAL:
    return bRtnValue;
}

void CQThreadHelper::WaitThread(void)
{
    if (0 == pthread_equal(m_nCallingTID, pthread_self()))
    {
        // instance를 생성한 caller thread와 다른 thread에서 호출을 방지한다.
        ERRORLOG("Invalid caller thread, %lu, %lu", (unsigned long int)m_nCallingTID, (unsigned long int)pthread_self());
        goto FINAL;
    }

    // 종료시까지 대기한다.
    INFOLOG("Wait for thread(%lu)", (unsigned long int)m_nThreadTID);
    wait(ULONG_MAX);
    INFOLOG("Finished to wait for thread(%lu)", (unsigned long int)m_nThreadTID);

FINAL:
    return;
}
해당 코드를 바라보면 자세한 slot/signal의 원리를 아는데 도움을 줄 겁니다.

이제 테스트 샘플을 확인해 보겠습니다.

release 1:
기존은 emit을 이용하여 thread간 통신을 하였으나, worker thread에서 UI primary thread로 호출이 되지 않고, worker thread에서 바로 호출되는 경우가 발견되었습니다. 이런 경우에 대비하여, emit대신, QMetaObject::invokeMethod를 호출하도록 수정하였습니다. 그 때는 worker thread에서 primary thread (혹은 connect를 호출한 thread)로 호출됨이 확인되었습니다.


위 프로젝트를 qtcreator에서 열어보고 실행한뒤, Start를 누르면 다음과 같습니다.

즉, Thread에서 1초, Handler에서 2초를 sleep하는데, ON_MESSAGE_SEND를 선택했기 때문에, 동기적으로 실행됨을 알 수 있습니다.
만일 ON_MESSAGE_POST를 선택하면 다음과 같이 비동기적으로 진행됩니다.

Try to start thread, parent=3079055120
Thread(3058555760) created
[THREAD] Try to Signal Emit : 0
[HANDLER] Thread Msg Signal Emitted, i=0, i+i=0
[THREAD] Try to Signal Emit : 1
[HANDLER] Thread Msg Signal Emitted, i=1, i+i=2
[THREAD] Try to Signal Emit : 2
[THREAD] Try to Signal Emit : 3
[HANDLER] Thread Msg Signal Emitted, i=2, i+i=4
[THREAD] Try to Signal Emit : 4
Thread(3058555760) finished
[HANDLER] Thread Msg Signal Emitted, i=3, i+i=6
[HANDLER] Thread Msg Signal Emitted, i=4, i+i=8

즉 동기적으로 진행되지 않으며, Thread가 종료된 이후에도 Handler가 실행됨을 알 수 있습니다.

참고로,
윈도우의 SendMessage와 마찬가지로 ON_MESSAGE_SEND 역시 ui thread와 worker thread간의 deadlock을 유발할 수 있으니 주의하시기 바랍니다. 그리고, 윈도우에서는 SendMessageTimeout이 있어 영원히 deadlock에 빠지는 것을 회피할 수 있으나 qt에서는 emit에 대한 timeout은 지원되지 않는거 같습니다.

(ps)
앞선 예는 모두 사용자 정의 message라 할 수 있습니다. 즉, 윈도우의 (WM_USER+xxxx) 경우입니다.
그것을 가지고 worker thread와 ui thread간에 Queued Message를 활용한 경우였습니다.
이와 다르게 이미 지원되는 Signal을 이용해 보도록 합니다.
이는 MFC의 WM_CLOSE와 OnClose같은 기본 정의 함수과 연관지을 수 있습니다.

만약 Thread가 종료되었을 때, ui에 해당 이벤트를 통보받아야 하는 경우를 가정해 보겠습니다.
윈도우 같은 경우에는, thread 함수 끝에 pcHwndUI->PostMessage(WM_THREAD_FINISHED,...)와 같은 형태로, 메시지를 새로 정의하여 전달하는 방식이었습니다. 그러나, Qt에서는 기본 이벤트를 제공합니다.


앞서 ON_MESSAGE_SEND를 통해 signal과 slot을 등록한다고 했는데, 해당 macro 내부를 보면 실제로는 QObject::connect라는 함수를 호출합니다.
QObject::connect는 다음의 속성을 입력 받습니다.

  • 발신지의 QObject pointer
  • 발신지의 Signal
  • 수신지의 QObject pointer
  • 수신지의 Slot
  • 메시지 전송 타입 (Queue방식? Blocked 방식?)

위에서 보면, Thread가 종료될 때 메시지를 수신해야 하기 때문에, m_cThread가 발신지가 됩니다. 그리고, Signal을 입력할 때, 자동으로 m_cThread가 지원되는 Signal이 발생합니다. 여기서는 finished를 연결할 수 있습니다. 물론, 해당 caller는 slot를 새로 만들어 연결해야 겠죠.

이와 같은 방법을 응용하면 기존의 slot/signal을 사용하는데 큰 문제는 없으리라 봅니다.

Trackback 0 Comment 3
  1. 진우 2013.11.11 10:23 address edit & del reply

    몇가지 궁금한게있습니다^^
    이벤트는 비동기 방식으로 처리 되어야할것같은데
    시그널-슬롯 방식은 비동기 방식인가요?
    boost.signals2 도 Qt 의 시그널 슬롯과 같은 방식인가요?

    • Favicon of https://greenfishblog.tistory.com BlogIcon 초록생선 2013.11.11 10:50 신고 address edit & del

      질문을 정확히 이해하기 힘드네요.

      이벤트의 동기/비동기는,
      Send/Post로 가정하겠습니다. (질문을 이렇게 해석했습니다.)

      동기는 즉각적인 event handler를 호출하고 호출자는 block이 되며,
      비동기는 event handler를 defer 처리하고, 호출자는 Pass되는것으로
      가정하겠습니다.
      비동기에서, 해당 부분의 처리를 위해 queue를 만들게 되는 것이고요.
      위 포스트에서 "Post" keyword가 비-동기에 해당되는 것입니다.

      혹시 비-동기 방식을, event handler가 다른 thread에서 돌리기를
      원한다는 뜻입니가? 그럼 iocp등을 알아보시는것도 좋을듯 합니다.

      qt 내부에서 boost를 사용하는것 같지는 않아보입니다(정확하지 않음).
      boost.signals2와 look and feel은 비슷해 보이네요.
      boost.signals2를 사용하셨다면, qt의 signal/slot을
      보다 빨리 이해할 수 있으리라 봅니다.
      qt를 사용하고 계신다면, 굳이 boost를 사용할 필요는 없겠습니다.
      왜냐하면, qt 개발 toolkit(예, qtcreator)의 IDE에서 boost를
      해석해주지 않을테니깐요.

  2. 진우 2013.11.11 11:51 address edit & del reply

    네 맞습니다. 동기는 이벤트 핸들러 리턴될때까지 대기, 비동기는 즉각 리턴되는걸 의미한거고요
    MFC 메세지맵 방식 이외의 다른 비동기Post 처리 방식이 있는지 궁금해서
    시그널-슬롯 방식이 MFC 메세지맵 처럼 메세지를 Post 하고 바로 리턴되는건지가 궁금했습니다.
    검색해보니 타입안정성 때문에 동기식으로 처리된다고 하네요
    네 부스트 시그널-슬롯이랑 이름뿐아니라 거의 비슷한것같아요
    댓글 감사합니다. ^^