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의 원리를 아는데 도움을 줄 겁니다.
이제 테스트 샘플을 확인해 보겠습니다.
기존은 emit을 이용하여 thread간 통신을 하였으나, worker thread에서 UI primary thread로 호출이 되지 않고, worker thread에서 바로 호출되는 경우가 발견되었습니다. 이런 경우에 대비하여, emit대신, QMetaObject::invokeMethod를 호출하도록 수정하였습니다. 그 때는 worker thread에서 primary thread (혹은 connect를 호출한 thread)로 호출됨이 확인되었습니다.
위 프로젝트를 qtcreator에서 열어보고 실행한뒤, Start를 누르면 다음과 같습니다.
만일 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 |