이번 포스팅에서는 윈도 MFC 환경에서 사용가능한 두 가지 타입의 스레드에 대해 알아보고 생성/동기화/종료 방법을 예제를 통해 알아보겠습니다.
리눅스 환경에서 thread를 사용해 보신 분들은 pthread_create와 같은 POSIX Thread 함수에 익숙하실 텐데요.
윈도 MFC 환경에서는 두 가지 타입의 스레드가 존재하며 사용 목적과 프로그램 성격에 따라 타입을
구분하여 사용할 수 있습니다.
1. 스레드를 사용하는 이유
프로그램 실행 시 프로그램의 진입점인 main 함수를 실행하는 한 개의 메인 스레드가 생성됩니다.
메인 스레드만 사용하여 프로그램을 개발하려고 하면 다음과 같은 문제점들이 있습니다.
- 특정 작업을 백그라운드화 하지 못해 작업 끝날 때까지 사용자와 상호작용 불가능(화면 멈춤)
- 싱글 코어 사용으로 인한 퍼포먼스 저하 및 컴퓨팅 자원 낭비
- 타이머 등 비동기 메커니즘 구현 불가
현실에 빗대어 키오스크가 고장 난 맥도널드로 간 상황을 설명해 보겠습니다.
카운터에서 캐셔(UI Thread)에게 햄버거 세트를 주문했는데 주문을 접수받은 캐셔가 주방으로 가서 햄버거도 만들고 감자도 튀기고 콜라도 따르는 상황을 가정해 봅시다. 그럼 내 뒤로 주문하러 방문한 사람은 캐셔가 주방에서 일하는 동안 대기하고 있어야 합니다. 주방에 별도로 일하는 조리사(Worker Thread)가 있다면 캐셔(UI Thread)는 카운터에서 주문을 받고 조리사(Worker Thread)가 완성하는 음식을 손님에게 내어주면 됩니다. 만약 주방에 조리가가 1명이 아니라 다수라면 음식이 나오는 속도는 더 빨라질 것입니다.
2. 스레드를 활용한 병렬 처리 유의점과 한계
스레드를 사용하면 필연적으로 스레드 간 동기화 기법을 사용해야 합니다.
스레드 동기화 메커니즘에는 세마포어, 뮤텍스, 스핀락, Reader-Writer Lock 등이 있으며 각 메커니즘에는 퍼포먼스 차이와 함께 장단점이 존재하므로 적절한 메커니즘을 선택하여 사용해야 합니다. 동기화 설계가 잘못되면 스레드 간 서로의 락을 획득하기 위해 무한정 대기하게 되는 교착상태(데드락)에 빠질 수 있음에 유의해야 합니다.
또한 암달의 법칙(Amdal's Law)도 참고하며 스레드 프로그램을 개발하길 권장합니다. 암달의 법칙은 코어 수를 2배로 늘려도 처리 성능은 2배가 되지 못하고 20~40% 정도의 성능 손실이 발생한다는 법칙입니다. 아무리 병렬화를 한다고 해도 결국에는 직렬화 구간이 존재할 수밖에 없으며 이 때문에 한계가 존재한다는 법칙입니다. 이를 프로그램 개발과 연관 지어 생각하면 스레드 프로그램 개발 시 직렬화 구간에서는 시간이 오래 걸리는 작업을 최소화하고 병렬로 처리할 수 있는 부분은 최대한 병렬화 하는 것이 좋습니다.
3. User-Interface thread, Worker thread 비교
MFC에는 두 가지 타입의 스레드, User-Interface thread(이하 UI thread)와 Worker thread, 를 사용할 수 있습니다.
UI thread는 주로 사용자와의 입출력을 처리하는데 많이 쓰입니다. Worker thread는 사용자와의 입출력이 필요 없이 백그라운드에서 계산작업과 같은 역할이 필요할 때 사용됩니다.
UI Thread는 CWinThread 클래스를 상속받아 구현하는데 Worker thread와의 차이점은 사용자로부터의 메시지 이벤트를 수신하는 메시지펌프가 기본적으로 구현되어 있습니다. 그래서 While문 등을 사용하여 별도의 메시지 처리 Loop를 구현하지 않고 이벤트 메시지와 이벤트 메시지 핸들러만 정의하여 메시지를 처리할 수 있습니다.
아래는 간략한 UI Thread 코드입니다. 메시지맵에 메시지와 핸들러를 등록해 두면 다른 스레드에서 PostThreadMessage() 호출을 통하여 UI Thread의 메시지핸들러를 호출할 수 있습니다.
이벤트와 달리 메시지는 메시지큐에 쌓여서 순차처리 되므로 대량의 패킷 데이터 처리에는 메시지 처리 오버헤드도 더 발생하여 퍼포먼스에 악영향을 미칠 수 있으므로 권장하지 않습니다. 예를 들어 천 개의 패킷이 도착했으면 메시지도 천 개가 발생하여 쌓이게 됩니다. 이벤트로 처리할 경우에는 패킷이 도착했음을 한 번만 이벤트 설정하면 수신 스레드에서 Queue가 Empty 상태일 때까지 알아서 동작합니다.
/* .h */
class CMiscEventLoop : public CWinThread
{
DECLARE_DYNCREATE(CMiscEventLoop)
public:
CMiscEventLoop(); // protected constructor used by dynamic creation
virtual ~CMiscEventLoop();
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
protected:
DECLARE_MESSAGE_MAP()
public:
void OnMsgLocalDiagConsole(WPARAM wParam, LPARAM lParam);
};
/* .cpp */
BEGIN_MESSAGE_MAP(CMiscEventLoop, CWinThread)
ON_THREAD_MESSAGE(MSG_ID_MISCEVENT_LOCAL_DIAG_CONSOLE, OnMsgLocalDiagConsole)
END_MESSAGE_MAP()
// CMiscEventLoop message handlers
void CMiscEventLoop::OnMsgLocalDiagConsole(WPARAM wParam, LPARAM lParam)
{
DPRINTF("ENTER\n");
theApp.m_Diag.vExec_LocalDiagCommandLine(wParam, lParam);
DPRINTF("LEAVE\n");
}
/* view.cpp */
theApp.m_pMiscEventLoop->PostThreadMessage(MSG_ID_MISCEVENT_LOCAL_DIAG_CONSOLE, (WPARAM)p, 0); /* WPARAM: TST_DIAG_REQUEST_ITEM, TODO: LPARAM:DESTINATION INFO STRUCTURE */
다음은 Worker thread 예제 코드입니다. Worker thread의 함수 원형은 static UINT 함수명(LPVOID)입니다.
클래스 내부에 정의해도 static 타입이므로 클래스 인스턴스에 포함되지 않습니다. 그래서 스레드 생성 시 파라미터로 theApp 등 필요한 객체 정보를 전달해 주어야 합니다.
Worker thread 구현을 보면 while (TRUE) 구문으로 Loop를 구성하고 있습니다.
이 예에서는 Worker thread도 PeekMessage 사용을 통해 UI thread처럼 메시지를 수신받고 처리할 수 있음을 보여줍니다.
이벤트 대기를 위해 사용할 수 있는 동기화 함수는 WaitForSingleObject(), WaitForMultipleObjects(), MsgWaitForMultipleObjectsEx(), GetQueuedCompletionStatus() 등이 있으며 목적에 따라 적절하게 선택하여 사용하면 됩니다.
/* .h */
class CRilServer
{
public:
static UINT ThdRilServerMgr(LPVOID pParam);
};
/* .cpp */
UINT CRilServer::ThdRilServerMgr(LPVOID pParam)
{
PTST_LPVOID_PARAMS pParams = (PTST_LPVOID_PARAMS)pParam;
SetEvent((HANDLE)(pParams->lpOpaque_3)); // send parameter copy completed signal to thread creator
DWORD dwWaitRet;
MSG msg;
while (TRUE) {
dwWaitRet = MsgWaitForMultipleObjectsEx(1, &hWSAEvent, RIL_SERVER_SOCKET_POOL_REFRESH_INTERVAL, QS_POSTMESSAGE, MWMO_INPUTAVAILABLE);
if ((WAIT_OBJECT_0 + 0) == dwWaitRet) { /* FD_ACCEPT event at hWSAEvent by hsoListen */
WSANETWORKEVENTS ne;
WSAEnumNetworkEvents(hsoListen, hWSAEvent, &ne);
if (ne.lNetworkEvents & FD_ACCEPT) {
int iRet;
iRet = pthis->m_RilSession.IncreaseAcceptSockets(hsoListen);
DPRINTF("hsoListen event signaled, iRet(%d) PoolCnt(%Iu), SessionCnt(%Iu)\n", iRet, pthis->m_RilSession.iGetCount_AcceptExPOOL(), pthis->m_RilSession.iGetCount_ConnectedSessions());
}
} else if ((WAIT_OBJECT_0 + dwNCOUNT) == dwWaitRet) { /* Thread Msg Event via PostThreadMessage Call */
if (!PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
continue;
}
if (MSG_ID_CRILSERVER_QUIT == msg.message) {
RIL_LOG_INFO("MSG_ID_CRILSERVER_QUIT, ThdRilServerMgr Exit.");
break;
}
PTST_RIL_SESSION_NODE prsn = (PTST_RIL_SESSION_NODE)msg.lParam;
if (MSG_ID_CRILSEREVR_SOCK_CONNECTED == msg.message) {
pthis->m_RilSession.vRegisterSocketToSessionObj(prsn);
}
else if (MSG_ID_CRILSERVER_SOCK_DISCONNECTED == msg.message) {
pthis->m_RilSession.vUnregisterSocketFromSessionObj(hsoListen, prsn);
}
}
else if (WAIT_TIMEOUT == dwWaitRet) {
/* FIXME: Pool Optimization at Server System Idle, not user defined time interval fixed periodically */
pthis->m_RilSession.vOptimizeAcceptExSocketPool();
continue;
}
else if (WAIT_FAILED == dwWaitRet) {
RIL_LOG_INFO("MsgWaitForMultipleObjectsEx WAIT_FAILED(%d). Exit Thread Loop.", GetLastError());
break;
}
else {
RIL_LOG_ERROR("MsgWaitForMultipleObjectsEx return not expected value (%d)", dwWaitRet);
}
}
4. UI thread 사용 방법
4.1 클래스마법사로 UI thread 코드 생성
클래스마법사에서 Add MFC Class 창을 띄워서 Base class를 CWinThread로 지정하여 생성하면 UI thread 코드를 작성할 수 있습니다. 메서드를 정의하고 해당 메서드와 메시지를 메시지 맵에 등록하면 됩니다.
4.2 UI thread 인스턴스 생성 및 종료
App.cpp의 InitInstance()에서 생성한다고 가정한 예제 코드입니다.
헤더파일에 UI thread 헤더파일을 참조 후 UI thread 포인터 변수를 선언합니다.
그리고 cpp에서 객체 생성 후 CreateThread() 메서드를 호출하면 됩니다.
UI thread 종료는 DestoryWindow()를 호출합니다. 종료 처리는 ExitInstance() 메서드에 구현합니다.
/* App.h */
#include "CUiThread.h"
class App : public CWinApp
{
public:
App() noexcept;
CUiThread* m_pCUiThread;
}
/* App.cpp */
BOOL App::InitInstance()
{
/* Create UI thread */
m_pCUiThread = new CUiThread();
m_pCUiThread->CreateThread();
}
UI Thread Context에서는 Sleep(), WaitForSingleObject() 호출하지 않도록 합니다. 다른 UI Thread도 함께 더 상위 레벨에서 윈도우 메시지 처리가 블로킹되어 UI Thread간 병렬처리가 되지 않고 순차처리 됩니다.
5. Worker thread 사용 방법
worker thread 함수를 작성하고 생성과 종료 방법을 알아보겠습니다.
5.1 Worker thread 코드 작성 및 스레드 생성/종료
static UINT 함수명(LPVOID) 형식의 함수를 작성합니다.
LPVOID 타입의 파라미터로 넘길 정보가 여러개일 경우를 대비하여 별도의 구조체 타입을 아래와 같이 정의하여 사용하고 있습니다.
typedef struct _LPVOID_PARAMS {
LPVOID lpOpaque_1;
LPVOID lpOpaque_2;
LPVOID lpOpaque_3;
LPVOID lpOpaque_4;
LPVOID lpOpaque_5;
_LPVOID_PARAMS() {
ZeroMemory(this, sizeof(struct _LPVOID_PARAMS));
}
} TST_LPVOID_PARAMS, * PTST_LPVOID_PARAMS; // LPVOID PARAMS TYPE for THREAD Creation
부모 스레드는 자식 스레드 생성코드 호출 후 바로 다음 작업을 하는 것이 아니라 자식 스레드로부터 생성완료 이벤트를 기다렸다가 다시 자식 스레드에게 시작 이벤트를 전송 후 다음 작업으로 넘어가는 것이 정석적인 방법입니다.
다음의 Worker thread 생성 코드를 참조하세요. 예제 스레드 원형은 staic UINT ThdRilTx(LPVOID)입니다.
부모와 자식 간 동기화를 위해서 임시 이벤트 핸들 Start, Created를 사용합니다.
부모와 자식간 동기화는 다음과 같습니다.
- 부모: AfxBeginThread 호출->WaitForSingleObject(Created)->SetEvent(Start)
- 자식: SetEvent(Created)->WaitForSingleObject(Start)
CWinThread* _m_pThdRilTx;
noyecube_errno_t CRilServer::RilInit(unsigned short shPortNo)
{
....생략...
/* Create TX Thread */
HANDLE hEvent_Thread_Start;
HANDLE hEvent_Thread_Created;
hEvent_Thread_Start = CreateEvent(NULL, CREATE_EVENT_MANUAL_RESET, FALSE, _T("hEvent_Thread_Start_Ril"));
if (NULL == hEvent_Thread_Start) {
AfxMessageBox(_T("Fail:NoyeCube:RilInit.hEvent_Thread_Start"));
this->RilDeInit();
return (-NOYECUBE_ECREATIONFAIL);
}
hEvent_Thread_Created = CreateEvent(NULL, CREATE_EVENT_MANUAL_RESET, FALSE, _T("hEvent_Thread_Created_Ril"));
if (NULL == hEvent_Thread_Created) {
AfxMessageBox(_T("Fail:NoyeCube:RilInit.hEvent_Thread_Created"));
this->RilDeInit();
return (-NOYECUBE_ECREATIONFAIL);
}
this->m_hEvent_Wakeup_Tx = CreateEvent(NULL, CREATE_EVENT_MANUAL_RESET, FALSE, _T("hEvent_Wakeup_Tx_Ril"));
if (NULL == (this->m_hEvent_Wakeup_Tx)) {
AfxMessageBox(_T("Fail:NoyeCube:ThdRilServerMgr.m_hEvent_Wakeup_Tx."));
this->RilDeInit();
return (-NOYECUBE_ECREATIONFAIL);
}
this->m_hEvent_Thread_Exit_Tx = CreateEvent(NULL, CREATE_EVENT_MANUAL_RESET, FALSE, _T("m_hEvent_Thread_Exit_Tx_Ril"));
if (NULL == (this->m_hEvent_Thread_Exit_Tx)) {
AfxMessageBox(_T("Fail:NoyeCube:ThdRilServerMgr.m_hEvent_Thread_Exit."));
this->RilDeInit();
return (-NOYECUBE_ECREATIONFAIL);
}
TST_LPVOID_PARAMS st_lpParams;
st_lpParams.lpOpaque_1 = (LPVOID)(&theApp);
st_lpParams.lpOpaque_2 = (LPVOID)(&(theApp.m_RilSever));
st_lpParams.lpOpaque_3 = (LPVOID)hEvent_Thread_Created;
st_lpParams.lpOpaque_4 = (LPVOID)hEvent_Thread_Start;
_m_pThdRilTx = AfxBeginThread(theApp.m_RilSever.ThdRilTx, (LPVOID)&st_lpParams);
if (nullptr == _m_pThdRilTx) {
AfxMessageBox(_T("AfxBeginThread ThdRilTx Error.\nQuit Program."));
RIL_LOG_ERROR("AfxBeginThread ThdRilTx Error.");
this->RilDeInit();
return (-NOYECUBE_ECREATIONFAIL);
}
if (NULL != hEvent_Thread_Created) {
DPRINTF("WAITING ThdRilTx ...");
WaitForSingleObject(hEvent_Thread_Created, INFINITE);
ResetEvent(hEvent_Thread_Created);
DPRINTF("Completed ThdRilTx.\n");
}
SetEvent(hEvent_Thread_Start); /* Signal to Entering TX Thread Loop*/
if (NULL != hEvent_Thread_Start)
CloseHandle(hEvent_Thread_Start);
if (NULL != hEvent_Thread_Created)
CloseHandle(hEvent_Thread_Created);
...생략...
return NOYECUBE_ESUCCESS;
}
Worker thread에서 SetEvent((HANDLE)(pParams->lpOpaque_3)) 코드가 생성완료 이벤트를 부모에 전달하는 코드입니다. WaitForSingleObject((HANDLE) pParams->lpOpaque_4)) 코드가 부모로부터 시작 명령을 대기하는 코드입니다.
while loop에서 WaitForMultipleObjects()로 이벤트 두 개를 사용하는데 그중 m_hEvent_Thread_Exit_Tx 이벤트가 발생하면 loop를 빠져나가고 스레드를 종료하도록 되어 있습니다.
즉 이 Worker thread를 종료하려면 SetEvent( m_hEvent_Thread_Exit_Tx)를 호출하면 됩니다.
UINT CRilServer::ThdRilTx(LPVOID pParam) // tx thread
{
PTST_LPVOID_PARAMS pParams = (PTST_LPVOID_PARAMS)pParam;
PTST_DSM_HEADER_ITEM p;
CHAR pktBuf[RIL_MSS_SIZE];
UINT16 pktBufUsedLen = 0;
const CNoyecubeApp* ptheApp = (CNoyecubeApp*)(pParams->lpOpaque_1);
CRilServer* pRilObj = (CRilServer*)(pParams->lpOpaque_2);
Concurrency::concurrent_queue<PTST_DSM_HEADER_ITEM>* pqDataTx = &(pRilObj->m_qDataTx);
BOOL boTryPopResult;
HANDLE arrhEvents[2];
DWORD dwEvent;
int iRet;
arrhEvents[0] = pRilObj->m_hEvent_Wakeup_Tx;
arrhEvents[1] = pRilObj->m_hEvent_Thread_Exit_Tx;
DPRINTF("Thread (TX TID:%d) created successfully.\n", GetCurrentThreadId());
SetEvent((HANDLE)(pParams->lpOpaque_3)); // send parameter copy completed signal to thread creator
WaitForSingleObject((HANDLE)(pParams->lpOpaque_4), INFINITE);
DPRINTF("hEvent_Thread_Start triggered. Enter Loop.(TX TID:%d)\n", GetCurrentThreadId());
ZeroMemory(pktBuf, sizeof(pktBuf));
pktBufUsedLen = 0;
while (TRUE) {
ResetEvent(arrhEvents[0]); /* m_hEvent_Wakeup_Tx */
p = nullptr;
boTryPopResult = pqDataTx->try_pop(p);
if (FALSE == boTryPopResult) {
dwEvent = WaitForMultipleObjects(2, arrhEvents, FALSE, INFINITE);
if ((WAIT_OBJECT_0 + 0) == dwEvent) { // m_hEvent_Wakeup_Tx
continue;
}
else if ((WAIT_OBJECT_0 + 1) == dwEvent) { // m_hEvent_Thread_Exit
DPRINTF("m_hEvent_Thread_Exit_Tx triggered. Exit Loop.(TX TID:%d)\n", GetCurrentThreadId());
break; // thread exit
}
else if (WAIT_FAILED == dwEvent) {
RIL_LOG_ERROR("WaitForMultipleObjects WAIT_FAILED(%d) but Ignored.", GetLastError());
continue;
}
else if (WAIT_TIMEOUT == dwEvent) {
RIL_LOG_ERROR("WaitForMultipleObjects WAIT_TIMEOUT but not defined any action Ignored.");
continue;
}
else {
RIL_LOG_ERROR("WaitForMultipleObjects return (%d). not defined return value.", dwEvent);
continue;
}
}
else { // TRUE == boTryPopResult
...생략...
}
다음은 Worker thread에게 종료이벤트를 전송하고 종료처리를 하는 예제코드입니다.
CWinThread 타입의 _m_pThdRilTx가 nullptr이 아니면 현재 생성된 Worker thread를 가리키고 있는 것입니다.
앞서 언급했듯이 SetEvent(m_hEvent_Thread_Exit_Tx)를 호출하면 Worker thread로 종료이벤트가 전달되고 loop를 탈출하며 thread가 종료됩니다. 스레드가 완전히 종료되었음은 WaitForSingleObject()로 스레드핸들에 발생하는 이벤트를 대기하고 있으면 됩니다. 사용한 이벤트 핸들 변수를 닫아주면 마무리됩니다.
if (nullptr != _m_pThdRilTx) {
DPRINTF("[DBG] ==> _m_pThdRilTx->SetEvent(m_hEvent_Thread_Exit_Tx)\n");
SetEvent(m_hEvent_Thread_Exit_Tx);
DPRINTF("[DBG] ==> Before WaitForSingleObject(_m_pThdRilTx->m_hThread, INFINITE)...\n");
WaitForSingleObject(_m_pThdRilTx->m_hThread, INFINITE);
DPRINTF("[DBG] ==> After WaitForSingleObject(_m_pThdRilTx->m_hThread, INFINITE)...\n");
_m_pThdRilTx = nullptr;
if (INVALID_HANDLE_VALUE != m_hEvent_Thread_Exit_Tx) {
CloseHandle(m_hEvent_Thread_Exit_Tx);
m_hEvent_Thread_Exit_Tx = INVALID_HANDLE_VALUE;
}
if (INVALID_HANDLE_VALUE != m_hEvent_Wakeup_Tx) {
CloseHandle(m_hEvent_Wakeup_Tx);
m_hEvent_Wakeup_Tx = INVALID_HANDLE_VALUE;
}
}
여기까지 윈도 MFC 환경에서 User Interface Thread와 Worker Thread의 차이점과 각 스레드 타입을
실제 생성하고 종료하는 방법을 예제코드를 통해 알아보았습니다.
'프로그래밍 > C | C++' 카테고리의 다른 글
IOCP 기반 다중 접속 및 데이터 처리-1 (0) | 2023.12.13 |
---|---|
Auto Lock/Unlock for Thread Syncronization (0) | 2023.12.10 |
Windows환경 INI 설정 파일 활용 (0) | 2023.10.11 |
Multiplexing Client Connections-1 (0) | 2023.06.30 |
Network bridge 방식으로 증권사 API 활용하기-2 (0) | 2023.06.21 |