티스토리 뷰

C++/MFC Programming

멀티스레드

루우지 2018. 4. 29. 16:26

스레드란?

OS에서 하나의 프로그램이 실행된다는것은 하나의 프로세스가 생성된다는 의미와 같다. 그렇다면 이 프로세스는 작업 관리자를 통해서 직접 볼수 있는 단위이다. 일반적으로 프로세스는 하나의 실행 파일이 실행될 때 만들어지며, 운영체재 내부적으로는 여러 논리적 코드를 관리하는 단위이기도 하다. 하지만 실행 단위를 논할 때 기본적으로 프로세스지만 더 정확히는 스레드이다.  


프로세스가 하나의 집이라 가정하면 집에 거주하는 사람은 스레드가 된다. 한집에 한명이 살수도 있지만 여러명이 사는 집도 있다. 이렇다면 제한적인 자원을 여러 구성원이 공유해서 사용해야하며 문제가 발생하지 않도록 묵시적인 규칙이 있어서 이에 따라 운영되어야 한다는 것이다. 멀티스레드 프로그래밍에서 이런 이슈처럼 서로 충돌하는일이 없도록 동기화가 중요하다.



작업자 스레드

MFC에서는 스레드를 생성하는 함수는 AfxBeginThread()함수이다. 이 함수를이용하면 특정 전역 함수를 별도의 스레드로 실행할 수 있다. 만일 여러번 같은 방식으로 호출하면 그 수만큼 스레드가 생성된다.



void CWorkerThreadDemoDlg::OnBnClickedButton1()

{

// TODO: Add your control notification handler code here

TCHAR szWinPath[MAX_PATH];

::GetWindowsDirectory(szWinPath, MAX_PATH);

lstrcat(szWinPath, _T("\\notepad.exe."));


SHELLEXECUTEINFO sei;

::ZeroMemory(&sei, sizeof(sei));

sei.cbSize = sizeof(sei);

sei.hwnd = NULL;

sei.lpFile = szWinPath;

sei.nShow = SW_SHOW;

sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;

sei.lpVerb = __TEXT("open");

sei.lpParameters = NULL;


if (::ShellExecuteEx(&sei))

{

::WaitForSingleObject(sei.hProcess, INFINITE);

}

}



위와같이 버튼을 클릭하면 메모장이 오픈되는 소스를 직접 코딩후 실행을 해보면 본 다이얼로그에 응답없음이 뜨는걸 확인할 수 있다. 그러한 이유는 메모장을 실행한 후 (::ShellExecuteEx())한 후 메모장이 종료될떄(::WaitForSingleObject())까지 프로그램이 정지되었기 때문이다. 

프로그램이 정지했다는건 메인메세지 루프가 멈추었음을 의미하고 그렇다면 큐에 쌓이는 메세지를 처리하지 못한다는것이다. 프로그램이 하얗게 변하게 되는데 WM_PAINT 메세지를 처리하지 못해서이기 때문이다. 


::GetWindowsDirectory() = 윈도우가 설치된 폴더의 경로르 알아내는 역할 (C:\Windows)

::GetSystemDirectory() = 윈도우 시스템폴더의 전체 경로릉랄아내는 역할 (C:\Windows\System32)

::ShellExecuteEx()함수는 특정 경로의 파일을 실행하는 함수

::WaitForSingleObject() = 특정 객채의 상태가 설정될 때까지 현재 스레드의 실행을 멈추는 역할을 한다.



이제 위의 소스코드를 스레드 형태로 바꿔보도록 하겠다.



UINT ThreadWitNotepad(LPVOID pParam)

{

// TODO: Add your control notification handler code here

TCHAR szWinPath[MAX_PATH];

::GetWindowsDirectory(szWinPath, MAX_PATH);

lstrcat(szWinPath, _T("\\notepad.exe."));


SHELLEXECUTEINFO sei;

::ZeroMemory(&sei, sizeof(sei));

sei.cbSize = sizeof(sei);

sei.hwnd = NULL;

sei.lpFile = szWinPath;

sei.nShow = SW_SHOW;

sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;

sei.lpVerb = __TEXT("open");

sei.lpParameters = NULL;


if (::ShellExecuteEx(&sei))

{

::WaitForSingleObject(sei.hProcess, INFINITE);

AfxMessageBox(_T("메모장이 종료 되었습니다."));

}


}


스레드에서 수행할 함수를 전역함수로 정의를 하게 되는데 안에 내용은 기존 소스코드와 동일하다. 다만 종료될 경우 메세지박스로 종료되었음을 알리나는 소스코드를 추가한다. 

그런 뒤 버튼이 클릭됬을 경우의 소스코드를 수정해준다.



void CWorkerThreadDemoDlg::OnBnClickedButton1()

{

// TODO: Add your control notification handler code here

//TCHAR szWinPath[MAX_PATH];

//::GetWindowsDirectory(szWinPath, MAX_PATH);

//lstrcat(szWinPath, _T("\\notepad.exe."));


//SHELLEXECUTEINFO sei;

//::ZeroMemory(&sei, sizeof(sei));

//sei.cbSize = sizeof(sei);

//sei.hwnd = NULL;

//sei.lpFile = szWinPath;

//sei.nShow = SW_SHOW;

//sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;

//sei.lpVerb = __TEXT("open");

//sei.lpParameters = NULL;


//if (::ShellExecuteEx(&sei))

//{

// ::WaitForSingleObject(sei.hProcess, INFINITE);

//}

CWinThread* pThread = AfxBeginThread(ThreadWitNotepad, NULL);

if (pThread == NULL)

{

AfxMessageBox(_T("ERROR: FAILED TO BEGIN WORKER-THREAD!"));

}


}




사용자 인터페이스 스레드

일반적인 윈도우 응용 프로그램을 작성할 때 최상위 프레임 윈도우는 보통 CMainFrame클래스 객체이다. 이 개체는 오로지 하나만 존재하는 것이 일반적이고 다른 윈도우아 달라 메인 메시지 루프와 연결되어 있다. 즉 응용 프로그램은 하나는 하나 이생의 스레드를 가지고 있으며, 하나의 최상위 프레임 윈도우는 응용 프로그램 전반에 관여하는 메인 메시지 큐를 갖는다.


다시 말해 최상위 프레임 윈도우 하나에 대해 별도의 스레드와 메시지 큐가 있다고 볼 수 있다. 그러므로 최상위 윈도우 하나를 생성한다는 것은 그에 관련된 별도의 스레드와 메시지 큐를 생성하는 것과 같다. 이들에 대한 코드를 작성하려면 많은 코드가 필요하지만 FMC에서는 AfxBeginThread()함수 하나로 가능하다. 


사용자 인터페이스 스레드를 생성하려면 CWinThread 클래스나 그 파생 클래스를 프로젝트에 등록한 후 InitInstance() 가상 함수를 재정의하여 CWinApp 클래스 객체의 InitInstance()함수와 같거나 유사한 코드를 작성하고, AfxBeginThread() 함수와 RUNTIME_CLASS() 매크로를 이용하여 해당 스레드의 인스턴스를 생성해야 한다.


1. CWinThread를 상속받는 새로운 클래스 CUIThread를 생성한다. 이 객체는 RUNTIME_CLASS() 매크로를 이용하여 생성한다.

2. 메뉴 툴바 리소스에 Create UI Thread 메뉴에 ID_MENU_CREATEUITHREAD로 하고 CMainFrame 클래스에 핸들러 함수를 등록하여 코드를 작성한다. 이때 MainFrame.cpp에 관련 헤더를 인크루드 해야한다.

#include "UIThread.h"


void CMainFrame::OnMenuCreateuithread()

{

AfxBeginThread(RUNTIME_CLASS(CUIThread));

}


3. CUIThread 클래스의 InitInstance() 함수에 다음과 같이 프레임 윈도우를 생성하는 소스를 작성한다.
BOOL CUIThread::InitInstnace()
{
CMainFrame* pFrame = new CMainFrame;
if(!pFrame)
return FALSE;
m_pMainWnd = pFrame;

pFrame->LoadFrame(IDR_MAINFRAME, WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE NULL, NULL);
pFrame->ShowWindow(SW_SHOW);
pFrmae->UpdateWindow();

return TREU;
}

4. CChildView 클래스의 OnPaint() 함수에 현제 스레드의 id르 ㅊ루력하는 코드를 추가한 후 빌드화여 결과 하긴
void CCHildView::OnPaint() 
{
CPaintDC dc(this);

CString strTmp = _T("");
strTmp.Foramt(_T("Current Thread ID : %D"), ::GetCurrentThreadId());
dc.TextOut(10, 10, strTmp);
}


스레드의 제어
AfxBeginThread()함수를 이용하여 스레드를 새엉하면 CWinThread 클래스 객체가 생성되어 반환된다. 이 객체를 이용하면 해당 스레드를 제어할수 있는데 이리적으로 중지 시키거나 재시작할 수 있다.


1. stdAfx.cpp와 .h을 열어 외부 변수를 선언한다.

CWinThread* g_pUIThread = NULL;

extern CWinThread* g_pUIThread;


2. CMainFrame 클래스의 OnMenuCreatuiThread()함수를 수정하여 스레드의 중복 실행을 막고 처음 생성된 스레드의 주소를 전역 변수에 저장한다.


if(g_pUIThread != NULL)

{

AfxMessageBox(_T("사용자 인터페이스 스레드가 실행중."));

return;

}


g_pUIThread = AfxBeginThread(RUNTIME_CLASS(CUIThread));


3. 사용자인터페이스 스레드가 종료될 때 전역 변수를 NULL로 초기화하도록 CUIThread클래스의 ExitInstance(0함수에 다음 과 같은 코드를 추가합니다.


int CUIThread::ExitInstance()

{

g_pUIThread = NULL;

return CWinThread::ExitInstance();

}


4. 리소스 ui에 Create UI Thread, Resume Thread, Suspencd Thread 메뉴를 생성하고 핸들러 함수를 등록한다.


void CMainFrame::OnMenuResumethread()

{

if(g_pUIThread != NULL)

g_pUIThread->ResumeThread();

}


void CMainFrame::OnMenuSuspendthread()

{

if(g_pUIThread != NULL)

g_pUIThread->SuspendThread();

}

void CMainFrame::OnMenuExitthread()

{

if(g_pUIThread != NULL)

g_pUIThread->PostThreadMessage(WM_QUIT, NULL, NULL);

}


CWinThread 클래스의 SuspendTHread() 메서드는 Sleep() 함숴럼 스레드의 실행을 일시적으로 중지한다. 그러나 중지하는 시간을 따로 명시할수는 없고 ResumeThread()메서드가 호출될 때까지 일시 중지를 상태를 유지한다. 그리고 PostThreadMessage()메서드는 CWnd 클래스의 PostMessage()함수와 기능은 같다. 그러나 메시지 전송 대상이 특정 윈도우가 아니라 특정 스레드의 메시지 큐라는점이 다르다.