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을 사용하는데 큰 문제는 없으리라 봅니다.

'프로그래밍 > QT' 카테고리의 다른 글

Visual Studio 2008로 qtwebkit 사용하기  (0) 2013.04.16
qtcreator로 memory leak 찾아내기  (0) 2012.11.08