// 데브피아(devpia) 가욱현, 정대원 님의 글을 토대로 함다.1. 개요
현재 대부분의 OS는 프로세스 스케쥴링에 의해 프로그램의 멀티태스킹(Multi-tasking)을 지원하고 있다. 멀티태스킹이란 실행되고있는 프로그램을 일정 단위로 잘라서(slice) 순서대로 CPU를 사용하게끔 하는 것 인데, 사용자는 마치 동시에 여러 개의 프로그램이 실행되는 것처럼 느낄 수 있게 된다. 즉, CPU 사용률을 최대화 하고, 대기시간과 응답시간의 최소화를 가능케 해주는 방법이다. 이번에는 프로세스 한 개만 놓고 보자. 한 프로세스는 구성면에서 [텍스트]-[데이터]-[스택] 영역으로 구성되어있고, 기능면에서는 텍스트의 모듈들은 각각의 역할을 가지고 있다. 프로세스에서의 공유메모리영역을 제외한 부분끼리 묶어서 쓰레드로 만든 후, 이것들을 멀티태스킹처럼 동작시키면 멀티쓰레딩이 되는 것이다. 멀티쓰레드 프로그램을 작성할 경우의 장점은 다음처럼 요약될 수 있다. 1) 병렬화가 증가되어 2) CPU사용률이 극대화되며, 3) 전체적 처리율이 빨라지고, 4) 사용자에대한 응답성이 향상된다. 5) 또한, 완벽에 가까운 기능별 구분에 의한 모듈작성을 함으로써 설계가 단순해져서, 6) 프로그램의 안정성이 향상된다. 7) 코드의 복사본을 여러 개 수행하여 여러 개의 클라이언트에서 동일한 서비스를 제공할수 있다. 8) 블록될 가능성이 있는 작업을 수행할 때 프로그램이 블록되지 않게 한다. 하지만, 쓰레드를 사용하면 오히려 불리한 경우도 있다. 대표적인 예로, 교착상태(deadlock)와 기아(starvation)이다. 쓰레드 기법을 사용할 때 주의사항을 정리하자면, 1) 확실한 이유를 가지고 있지 않는 경우에는 쓰레드를 사용하면 안 된다. 즉 쓰레드는 명확히 독립적인 경우에 사용해야 한다. 2) 명확히 독립적인 쓰레드라 하여도 오히려 나눔으로 인해 OS가 쓰레드를 다루는데에 따른 부하(overload)가 발생하게 된다. 즉, 실제 쓰레드에 의해 수행되는 작업량보다 클 경우에는 사용하지 않도록한다. 멀티쓰레드를 이용한 애플리케이션을 작성하는 구조에는 3가지 방법이 있다.. 1. boss/worker 모델.. 2. work crew 모델. 3. pipeline 모델. 1. 첫번째 쓰레드(주쓰레드)가 필요에 따라 작업자 쓰레드를 만들어 내는 경우. 이런 경우는 C/S 환경에서 접속받는 부분을 쓰레드로 돌리고, 접속요청이 오면 새로운 쓰레드를 만들어 사용자와 연결시켜 주는 방법이다. 이때 접속 받는 쓰레드가 주 쓰레드(boss Thread) 라고 하고, 사용자와 연결된 다른 쓰레드.. 즉 주 쓰레드로부터 실행된 쓰레드는 작업자 쓰레드(worker Thread) 라고 한다.. 2. 두번째 방식은 어떤 한 작업을 여러 개의 쓰레드가 나눠서 하는 방식이다. 즉 집을 청소한다는 개념의 작업이 있으면, 청소하는 작업에 대한 쓰레드를 여러 개 돌리는 거.. 3. 공장라인을 생각... 쓰레드는 UI(User Interface) Thread와 Worker(작업자) Thread로 나뉜다. UI Thread는 사용자 메시지 루프를 가지고 있는(즉 어떤 메시지가 날라오면 일하는.. )쓰레드이고.. Worker Thread는, 보통 오래 걸리는 작업이나 무한루프를 가지는 작업을 하는 사용자 정의 함수의 경우 사용. UI Thread를 사용하려면, CWinThread 파생 클래스를 만들어 사용한다. MFC에서는 AfxBeginThread의 서로 다른 버전 두 개를 정의 하고 있다.. 하나는 작업자 쓰레드를 위한 것이고, 하나는 UI쓰레드를 위한 것이져.. 원형은 다음과 같다.. UINT ThreadFunc(void* pParam) 이함수는 정적(static)클래스 멤버 함수 이거나 클래스 외부에서 선언한 함수여야 한다.2. 쓰레드의 기본
1) 쓰레드 생성
WM_CREATE 에서 쓰레드를 만들면 되는데 함수는 다음과 같다. HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId); +lpThreadAttributes : 쓰레드의 보안속성 지정. 자식 프로세스로 핸들을 상속하지 않은 한 NULL +dwStackSize : 쓰레드의 스택 크기 지정. 안정된 동작을 위해 쓰레드마다 별도의 스택 할당. 0으로 설정하면 주 쓰레드(CreateThread를 호출한 쓰레드)와 같은 크기를 갖으며, 스택이 부족할 경우 자동으로 스택크기를 늘려주므로 0으로 지정하면 무리가 없다. +lpStartAddress : 쓰레드의 시작함수를 지정. 가장 중요한 인수. +lpParameter : 쓰레드로 전달할 작업 내용이되 인수가 없을경우 NULL임. +dwCreationFlags : 생성할 쓰레드의 특성 지정. 0이면 아무 특성없는 보통 쓰레드가 생성되고 CREATE_SUSPENDED 플래그를 지정하면 쓰레드를 만들기만 하고 실행은 하지 않도록하고 실행을 원하면 ResumeThread함수를 호출하면 된다. +lpThreadId : 쓰레드의 ID를 넘겨주기 위한 출력용 인수이므로 DWORD형의 변수 하나를 선언한 후 그 변수의 번지를 넘기면 됨. **** 작업자 쓰레드 생성하기 **** 작업자 쓰레드로 특정한 작업을 하는 사용자 정의 함수를 맹글기 위해서, 윈도우에서는 여러가지 쓰레드 생성 함수를 제공해 준다. 그 함수의 종류를 알아보도록 하져.. 1. CreateThread() 2. _beginthread(), _beginthreadex() 3. AfxBeginThread(), AfxBeginThreadEx() 이렇게 약 5가지의 쓰레드 생성함수가 존재한다. 이제부터 저 5가지 함수의 특징을 알아보도록 하져….. 그럼 첫번째 CreateThread()함수. 이 함수는 보통 사용할때 다음과 같이 사용한다. HANDLE handle; Handle = CreateThread( Threadfunc(), Param ); 첫번째 인자는 사용자가 쓰레드로 돌려야할 작업함수를 써주는 곳이고, 두번째는 작업함수에 인자값으로 전해줄 값이 들어간다.. 이 인자값 형은 VOID*으로 되어 있기 때문에 4BYTE 이내의 값은 어떤 값이든 들어갈수 있져..대신 TYPE CASTING을 해주어야 하져.. 그리고 받는 쪽에서도 type casting를 해서 받아야 한다. 이함수가 올바르게 실행이 되면 쓰레드에 대한 핸들을 반환하는데.. 이 핸들을 가지고 쓰레드를 조작할 수가 있져.. 대표적으로 쓰레드를 닫을 때 CloseHandle()함수를 사용해서 쓰레드 핸들을 넣어주고 쓰레드를 닫아 주어야 한다.. 이함수로 생성된 쓰레드를 닫을때는 ExitThread() 면 됩니다. 그럼..두번째 _beginthread를 알아보도록 하져..CreateThread는 쓰레드에서 win32 API함수만 호출할수 있다.. 즉, 사용자가 어떤작업을 하는 함수를 만들 때 그 함수 안에서 win32API만 사용할수 있다는 말이다.. 즉 C함수나 MFC는 저얼대~~ 못 쓴다…. _beginthread 함수는 win32 API아 C 런타임 함수를 사용할 때 사용한다. 이 함수를 사용하면 C런타임 라이브러리가 핸들을 자동으로 닫으므로 이를 직접할 필요는 없다. 대신 _beginthreadex는 스레드 핸들을 직접 닫아야 한다. 그리고 이 쓰레드를 닫을 때는 _endthread(), _endthreadex()를 사용하면 된다. 세번째 AfxBeginThread()와 AfxBeginThreadEx().. 실질적으로 가장 자주 사용하는 쓰레드 생성함수이다.. 이 함수를 이용하면 사용자 정의 함수내에서 MFC, win32 API, C 런타임 라이브러리등 여러가지 라이브러리 함수들을 전부 사용할수 있다.. 주로 프로젝트를 MFC로 만들 때 사용하죠.. 이 함수는 리턴값이 CWinThread* 형을 리턴하며, 이 함수와 매칭되는 종료함수는 AfxEndThread()이다… 해서 쓰레드가 종료되면 MFC는 쓰레드 핸들을 닫고 리턴값으로 받은 CWinThread*객체를 제거한다. CWinThread* pThread = AfxBeginThread( Threadfunc, &threadinfo ); 첫번째 인자는 사용자 정의 함수이고, 두번째는 첫번째 인자의 쓰레드 함수에 인자값으로 들어갈 파라미터이다.. 이 형은 void* 형으로 4byte를 가지므로 어떤 형으로 넣어줄 때 type casting하면 된다…. 그 예는 다음과 같다.int nNumber = 1000; CWinThread *pThread = ::AfxBeginThread(ThreadFunc, &nNumber); UINT ThreadFunc(LPVOID pParam) { int j = (int)pParam; for (int i=0; i<j; i++) { // 수행할 작업 } }작업자 스레드 함수에 4바이트 이상의 정보를 넘겨주어야 할 경우에는 다음과 같이 작업자 스레드 함수에 넘겨주어야 할 모든 값을 포함하는 구조체를 선언하고,typedef struct tagTREADPARAMS { CPoint point; BOOL *pContinue; BOOL *pFriend; CWnd *pWnd; } THREADPAPAMS; // 그런 다음 구조체에 필요한 값들을 설정하고, 이 구조체의 포인터를 넘겨준다. THREADPAPAMS *pThreadParams = new THREADPAPAMS; // new로 할당 pThreadParams->point = m_ptPoint; pThreadParams->pContinue = &m_bExec; // 쓰레드 실행 플래그 pThreadParams->pFriend = &m_bYield; // 쓰레드 양보 플래그 pThreadParams->pWnd = this; m_pThread = AfxBeginThread(ThreadFunc, pThreadParams); UINT ThreadFunc(LPVOID pParam) { // 넘어온 인자를 복사 THREADPAPAMS *pThreadParams = (THREADPAPAMS *)pParam; CPoint point = pThreadParams->point; CWnd *pWnd = pThreadParams->pWnd; BOOL *pContinue = pThreadParams->pContinue; BOOL *pFriend = pThreadParams->pFriend; delete pThreadParams; // delete로 해제 // "실행" 플래그가 TRUE인 동안 스레드가 실행됨 while(*pContinue) { // 수행할 작업 // "양보" 플래그가 TRUE이면 다른 스레드에 CPU를 양보 if(*pFriend) Sleep(0); } return 0; }자 그럼..정리해 보도록 하져…..쓰레드를 생성하는 함수들은 크게 3가지가 있고..(확장된것까지 생각하면 5개..^^ ) 이들 함수의 특징은 다음과 같다. 쓰레드가 win32 API만을 사용한다면 CreateThread()를 사용하면 되고, C런타임 라이브러리를 사용하다면 _beginthread()를 사용하고, 전부다 사용한다면 AfxBeginThread()를 사용하면 된다.2) 쓰레드 종료
작업 쓰레드가 종료되었는지 조사하는 함수는 다음과 같다. BOOL GetExitCodeThread(HANDLE hThread, PDWORD lpExitCode); +hThread : 쓰레드의 핸들 +lpExitCode : 쓰레드의 종료코드. +Return : 계속 실행중 : STILL_ACTIVE, 쓰레드 종료 : 스레드 시작함수가 리턴한 값 or ExitThread 함수의 인수 쓰레드가 무한루프로 작성되어 있다해도 프로세스가 종료되면 모든 쓰레드가 종료되므로 상관이 없다. 백그라운드 작업을 하는 쓰레드는 작업이 끝나면 종료되는데 때로는 작업도중 중지해야 할 경우에는 다음 두 함수가 사용된다. VOID ExitThread(DWORD dwExitCode); BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); ExitThread는 스스로 종료할 때 사용.인수로 종료코드를 넘김. 종료코드는 주 쓰레드에서 GetExitCodeThread함수로 조사할 수 있다. 이것이 호출되면 자신의 스택을 해제하고 연결된 DLL을 모두 분리한 후 스스로 파괴된다. TerminateThread는 쓰레드 핸들을 인수로 전달받아 해당 쓰레드를 강제종료시킨다. 이 함수는 쓰레드와 연결된 DLL에게 통지하지 않으므로 DLL들이 제대로 종료처리를 하지 못할 수 있고 리소스도 해제되지 않을 수 있다. 그래서 이 작업 후 어떤일이 발생할지를 정확히 알때에만 사용하도록한다. 스레드를 죽이는 방법엔 두가지가 있져.. 1. 스레드 내부에서 return을 시킬 때. 2. AfxEndThread를 호출할 때. 안전한 방법은 스레드 내부 자체에서 return문을 이용해서 죽여주는게 안전하다. 위의 예와 같이... 다음은 쓰레드를 종료하는 함수의 예이다.if(m_pThread != NULL) { HANDLE hThread = m_pThread->m_hThread; // CWinThread *m_pThread; m_bExec = FALSE; // 실행 플래그를 FALSE로 하여 쓰레드 종료시킴.. ::WaitForSingleObject(hThread, INFINITE); // 이후 정리작업... }위의 첫번째 방법과 같이 return을 받았을때는 GetExitCodeThread를 이용해서 검색할수 있는 32bit의종료 코드를 볼수 있다.. DWORD dwexitcode; ::GetExitCodeThread( pThread->m_hThread, &dwExitCode ); // pThread는 CWinThread* 객체의 변수.. 만약 실행중인 스레드를 대상으로 저 코드를 쓰게 된다면 dwExitCode에는 STILL_ACTIVE라는 값이 들어가게 된다. 근데..위의 코드를 사용함에 있어 제약이 좀 있다. CWinThread*객체는 스레드가 return 되어서 종료가 되면 CWinThread객체 자신도 제거되어 버린다..즉 동반자살이져.. delete시켜주지 않아도 메모리에서 알아서 없어진다는 말이져.. 즉…return이 되어서 이미 죽어버린 스레드를 가지고 pThread->m_hThread를 넣어주면, Access위반이란 error메시지가 나오게 되져.. 이런 문제를 해결할라면 CWinThread* 객체를 얻은 다음 이 객체의 멤버 변수인 m_hAutoDelete를 FALSE로 설정하면 스레드가 return을 해도 CWinThread객체는 자동으로 제거 되지 않기 때문에 위의 코드는 정상적으로 수행이 된다.. 이런 경우에 CWinthread*가 더 이상 필요가 없어지면 개발자 스스로 CWinThread를 delete시켜 주어야 한다. 또다른 방법으로 스레드가 가동이 되면 CWinThread*의 멤버변수인 m_hThread를 다른 곳으로 저장을 해놓고 이 것을 직접GetExitCodeThread()에 전달을 하면 그 쓰레드가 실행중인지 한때는 실행되고 있었지만 죽어버린 스레드인지 확인이 가능하다.int a = 100; // 파라미터로 넘겨줄 전역변수. CWinThread* pThread // 전역 쓰레드 객체의 포인터 변수. HANDLE threadhandle; // 스레드의 핸들을 저장할 핸들변수. Initinstance() // 프로그램초기화. { // 프로그램 실행과 동시에 스레드 시작. 1번방법:pThread = AfxBeginThread( func, (int) a ); // 스레드가 리턴되면 자동으로 CWinThread객체가 자동으로 파괴되지 않게 설정. 2번방법:pThread->m_hAutoDelete = FALSE; // 쓰레드 핸드를 저장. 위의 m_hAutoDelete를 설정하지않았을경우.. threadhandle = pThread->m_hThread; } MessageFunction() // 어떤 버튼을 눌러서 스레드의 상태를 알고 싶다.. { char* temp; DWORD dwExitcode; // 스레드 객체의 m_hAutoDelete를 fasle로 설정해서 스레드가 return되어도 // 객체가 자동으로 파괴되지 않아서 핸들을 참조 할수 있다. 1번방법: ::GetExitCode( pThread->m_hThread, &dwExitcode); // 스레드가 종료되고 미리 저장해둔 핸들을 이용할경우.. 2번방법:::GetExitCode(threadhandle, &dwExitcode); sprintf( temp, "Error code : %d", dwExitcode ); // 스레드 객체 삭제.. 1번방법: delete pThread; AfxMessageBox( temp ); } func( void* pParam ) { int b = (int) pParam; for( int i = 0; i < b; i++) { // 어떤일을 한다. } return; // 작업이 끝나면 리턴한다. 이때 스레드 자동으로 종료. }1번째 방법은 스레드를 생성하고 m_hAutoDelete를 false로 해서 스레드가 return해서 자동종료해도 CWinthread를 자동파괴하지 않게 하고, GetExitCodeThread()를 호출하져.. 밑에서 delete해 주는 거 꼭 해야되고요..안그럼 메모리 누수가 되져.. 2번째는 m_hThread를 다른 핸들변수에 저장해 놓고..스레드가 return되면 CWinThread*도 같이 파괴가 되는데.. 원래 저장한 핸들을 가지고 GetExitcodeThread()를 호출해서 한때 존재했지만 종료된 쓰레드를 검사하는 것이져….이해 OK?????3) 대기 함수
WaitForSingleObject(), WaitForMultipleObjects()의 원형은 다음과 같다. DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds); DWORD WaitForMultipleObjects( DWORD nCount, // number of handles in array CONST HANDLE *lpHandles, // object-handle array BOOL bWaitAll, // wait option DWORD dwMilliseconds // time-out interval ); 쓰레드 종료를 위한 플래그를 설정한 후, 쓰레드가 완전히 종료된 것을 확인 후에 어떤 작업을 하고 싶으면 다음과 같이 한다.if (::WaitForSingleObject(pThread->m_hThread, INFINITE)) { // 쓰레드가 종료된 후 해야 할 작업들 }(쓰레드 종료를) 어느 정도 기다리다가 프로그램을 진행시키려면 다음과 같이 한다.DWORD dwRetCode; dwRetCode = ::WaitForSingleObject(pThread->m_hThread, 2000); if (dwRetCode == WAIT_OBJECT_0) { // 쓰레드가 종료된 후 해야 할 작업들 } else if(dwRetCode == WAIT_TIMEOUT) { // 2초 동안 쓰레드가 종료되지 않았을 때 해야 할 에러 처리 }다음과 같이 하면, 어떤 쓰레드가 현재 실행 중인지 아닌지를 알 수 있다.if (::WaitForSingleObject(pThread->m_hThread, 0) == WAIT_TIMEOUT) { // 현재 쓰레드가 실행 중. } else // 실행 중인 상태가 아니다.// WaitForMultipleObjects() sample... // 쓰레드 함수의 원형 DWORD WINAPI increment(LPVOID lParam); DWORD WINAPI decrement(LPVOID lParam); int main() { // char* ps[] = {"increment", "decrement"}; DWORD threadID; HANDLE hThreads[2]; // hThreads[0] = CreateThread( NULL, 0, increment, (LPVOID)ps[0], 0, &threadID); // hThreads[0] = CreateThread( NULL, 0, increment, NULL, 0, &threadID); for (int i=0; i<2; ++i) { hThreads[i] = CreateThread( NULL, 0, increment, (void *)i, 0, &threadID); } // 모든 쓰레드가 종료할 때 까지 기다린다. // WaitForMultipleObjects(2, hThreads, TRUE, INFINITE); int ret; ret = WaitForMultipleObjects(2, hThreads, FALSE, INFINITE); switch(ret) { case WAIT_OBJECT_0: // handle hThreads[0] is signaled.. break; case WAIT_OBJECT_0+1: break; } CloseHandle(hThreads[0]); CloseHandle(hThreads[1]); return 0; } DWORD WINAPI increment(LPVOID lParam) { while (1) { ... } return 0; } DWORD WINAPI decrement(LPVOID lParam) { while (1) { ... } return 0; }4) 쓰레드 일시중지 - 재개
DWORD SuspendThread(HANDLE hThread); - 1 DWORD ResumeThread(HANDLE hThread); - 2 둘 다 내부적으로 카운터를 사용하므로 1을 두번 호출했다면 2도 두번 호출해야한다. 그래서 카운터가 0 이되면 쓰레드는 재개하게된다.5) 우선순위 조정
향상된 멀티태스킹을 지원하기 위해서는 시분할 뿐만 아니라 프로세스의 우선순위를 지원해야 한다. 마찬가지로 프로세스 내부의 쓰레드들도 우선순위를 갖아야 하며 우선순위 클래스, 우선순위 레벨 이 두 가지의 조합으로 구성된다. 우선순위 클래스는, 스레드를 소유한 프로세스의 우선순위이며 CreateProcess 함수로 프로세스를 생성할 때 여섯번째 파라미터 dwCreationFlag로 지정한 값이다. 디폴트는 NORMAL_PRIORITY_CLASSfh 보통 우선순위를 가지므로 dwCreationFlag를 특별히 지정하지 않으면 이 값이 전달된다. 우선순위 레벨은 프로세스 내에서 쓰레드의 우선순위를 지정하며 일단 쓰레드를 생성한 후 다음 두 함수로 설정하거나 읽을 수 있다. BOOL SetThreadPriority(HANDLE hThread, int nPriority); Int GetThreadPriority(HANDLE hThread); 지정 가능한 우선순위 레벨은 총 7가지 중 하나이며 디폴트는 보통 우선순위인 THREAD_PRIORITY_NORMAL 이다. 우선순위 클래스와 레벨값으로부터 조합된 값을 기반우선순위(Base priority)라고 하며 쓰레드의 우선순위를 지정하는 값이 된다. 기반우선순위는 0~31 중 하나이며 0은 시스템만 가질 수 있는 가장 낮은 우선순위 이다. (낮을수록 권한이 높음) 우선순위를 높이는(에이징)방법과 낮추는 방법을 동적 우선순위 라고하며, 우선순위 부스트(Priority Boost)라고 한다. 단 이 과정은 기반 우선순위 0~15 사이의 쓰레드에만 적용되며 16~31 사이의 쓰레드에는 적용되지 않는다. 또한 사용자입력을 받거나(인터럽트) 대기상태에서 준비상태가 되는 경우에는 우선순위가 올라가고, 쓰레드가 할당된 시간을 다 쓸 때마다 우선순위를 내려 결국 다시 기반 우선순위와 같아지게 되는데, 어떠한 경우라도 동적 우선순위가 기반 우선순위보다는 더 낮아지지 않는다.
Thread 사용방안.
'프로그래밍 > VC++ 개발 코딩' 카테고리의 다른 글
Window Message 함수 설명 - MSDN (0) | 2014.01.17 |
---|---|
TerminateThread 사용하는 경우 메모리 증가 처리방안 (0) | 2014.01.17 |
GetMessage / PeekMessage 차이 설명 (0) | 2014.01.17 |
본문스크랩] [Tip] MFC에서 C# UserControl 사용하기 (0) | 2014.01.17 |
마우스 라인 그리기 (0) | 2014.01.17 |