// 데브피아(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 사이의 쓰레드에는 적용되지 않는다.
또한 사용자입력을 받거나(인터럽트) 대기상태에서 준비상태가 되는 경우에는 우선순위가 올라가고,
쓰레드가 할당된 시간을 다 쓸 때마다 우선순위를 내려 결국 다시 기반 우선순위와 같아지게 되는데,
어떠한 경우라도 동적 우선순위가 기반 우선순위보다는 더 낮아지지 않는다.
3. 쓰레드간 동기화
멀티쓰레드는 개요에서 말했듯이 한 프로세스를 여러 역할에 따라 여러 개의 쓰레드로 나뉘어 작업하는 방식이므로 각 쓰레드간의 동기화가 필요하다.
동시에 복수개의 코드가 같은 주소영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번하기 때문이다.
멀티쓰레드의 가장 큰 문제점은 공유자원(주로 메모리의 전역변수)을 보호하기가 어렵다는 점이다.
그리고 쓰레드간의 실행순서를 제어하는 것도 쉽지 않은 문제이다.
이런 여러가지 문제점을 해결하기 위하여 쓰레드간의 실행 순서를 제어할 수 있는 여러가지 방법들을 동기화라고 한다.
동기화 방법에는, Interlocked, 임계영역, 뮤텍스, 세마포어, 이벤트등의 기법을 사용한다.
1) 임계영역 (Critical Section)
동기화문제를 해결하는 방법들 중 가장 쉬운반면 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
임계영역(Critical Section)이란 공유자원의 독점을 보장하는 코드의 영역을 가리킨다. 이는 아래 두 함수로 시작하고 끝낸다.
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
CRITICAL_SECTION형의 포인터형은 복수개의 쓰레드가 참조해야 하므로 반드시 전역변수로 선언해야한다. 사용법은 다음과 같다.
CRITICAL_SECTION crit1, crit2;
함수 {
…
EnterCriticalSection(&crit1);
//공유자원1을 액서스한다.
LeaveCriticalSection(&crit1);
EnterCriticalSection(&crit2);
//공유자원2을 액서스한다.
LeaveCriticalSection(&crit2);
…
}
주의할것은 가급적 임계영역 내부의 코드가 빨리 끝날 수 있도록 짧은 시간을 사용하도록 작성해야 한다.
만약 Leave를 호출하지않고 쓰레드를 빠져나와버리면 이후부터는 다른 쓰레드는 이 임계영역에 들어갈 수 없게된다.
만약 이부분에서 예외가 발생하여 Leave함수가 호출되지 못하게 될 수도 있다.
그래서 임계영역을 쓸 때는 반드시 구조적 예외 처리구문에 포함시켜주는 것이 좋다.
Try {
EnterCriticalSection(&crit);
…
}
finally {
LeaveCriticalSection(&crit);
}
이렇게하면 설사 예외가 발생하더라도 Leave함수는 반드시 호출되므로 훨씬 안전해진다.
다음은 MFC 에서의 사용 예이다.
CCriticalSection g_critical; // 전역 변수로 선언
function()
{
AfxBeginThread(ThreadFuncA, NULL);
AfxBeginThread(ThreadFuncB, this);
}
UINT ThreadFuncA(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncA가 할 일....
g_critical.Unlock();
}
return 0;
}
UINT ThreadFuncB(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncB가 할 일....
g_critical.Unlock();
}
return 0;
}
2) 뮤텍스(Mutex)
임계영역은 앞서 말했듯 동일한 프로세스 내에서만 사용할 수 있다.
그러나, 뮤텍스(Mutex; Mutual Exclusion;상호배제)는 임계영역이 사용된 곳에 대신 사용될 수 있으며, 프로세스 간에도 사용할 수 있다.
뮤텍스를 사용하려면 다음 함수로 생성해야 한다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL blInitialOwner, LPCTSTR lpName);
lpMutexAttributes : 보안속성. 대개 NULL
blInitialOwner : 뮤텍스 생성과 동시에 소유할 것인지 지정.
lpName: 뮤텍스의 이름을 지정하는 문자열.
뮤텍스는 프로세스간의 동기화에도 사용되므로 이름이 필요하고, 이 이름은 프로세스간 뮤텍스를 공유할 때 사용된다.
뮤텍스 소유를 해지하여 다른 쓰레드가 이것을 가질 수 있도록 하려면 임계영역의 LeaveCriticalSection 에 해당하는 다음 함수를 호출하면 된다.
BOOL ReleaseMutex(HANDLE hMutex);
만일 프로세스가 다른 프로세스의 쓰레드에 의해서 이미 생성된 뮤텍스의 핸들을 얻기를 원하거나,
뮤텍스가 존재하지 않는 경우에 뮤텍스를 생성하기 원한다면 다음 함수를 사용한다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
3) 세마포어 (Semaphore)
세마포어도 뮤텍스와 유사한 동기화 객체이나 다른점은, 뮤텍스는 하나의 공유자원을 보호하기 위해 사용하지만,
세마포어는 제한된 일정 개수를 가지는 자원(HW, 윈도우, 프로세스, 쓰레드, 권한, 상태 등 컴퓨터에서의 모든 자원)을 보호하고 관리한다.
세마포어는 사용 가능한 자원의 개수를 카운트하는 동기화 객체이다.
세마포어와 관련된 함수는 다음과 같다.
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG IlInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
4) 이벤트 (Event)
임계영역, 뮤텍스, 세마포어는 주로 공유자원을 보호하기 위해 사용되는 데 비해
이벤트는 이보다는 스레드간의 작업순서나 시기를 조정하기 위해 사용한다.
특정한 조건이 만족될 때까지 대기해야 하는 쓰레드가 있을 경우 이 쓰레드의 실행을 이벤트로 제어할 수 있다.
이벤트는 자동리셋과 수동리셋이 있다.
+자동 리셋 이벤트 : 대기상태가 종료되면 자동으로 비신호상태가 된다.
+수동 리셋 이벤트 : 쓰레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.
++신호상태 (Signaled): 쓰레드 실행가능상태. 신호상태의 동기화 객체를 가진 쓰레드는 계속 실행할 수 있다.
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
bManualReset은 이벤트가 수동리셋 이벤트(manual)인지 자동리셋 이벤트(automatic)인지 지정하는데 TRUE이면 수동리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 쓰레드가 곧바로 실행을 하도록 해준다.
이벤트도 이름(lpName)을 가지므로 프로세스간의 동기화에 사용될 수 있다.
또한 이벤트가 임계영역이나 뮤텍스와 다른점은
대기함수를 사용하지 않고도, 쓰레드에서 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다. 다음 함수를 사용한다.
BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);
SetEvent는 신호상태로 만들고 ResetEvent는 비신호상태로 만든다.
다음은 MFC 에서의 사용 예이다.
CEvent g_event; // 전역변수로 선언
FunctionA()
{
AfxBeginThread(ThreadFunc, this);
}
FunctionB()
{
g_event.SetEvent(); // Lock() 함수에서 더 이상 진행하지 못하고 잠자고 있는 쓰레드를 깨워서 일을 시키려면 SetEvent()를 호출.
}
// ThreadFunc() 함수는 이벤트가 발생할 때마다 while문을 한번씩 실행.
UINT ThreadFunc(LPVOID pParam)
{
while(1)
{
g_event.Lock(); // SetEvent()가 호출되면, Lock()함수에서 실행이 중단된 쓰레드가 다음 코드를 실행.
// ThreadFunc가 할 일....
g_event.Unlock();
}
return 0;
}
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 사이의 쓰레드에는 적용되지 않는다.
또한 사용자입력을 받거나(인터럽트) 대기상태에서 준비상태가 되는 경우에는 우선순위가 올라가고,
쓰레드가 할당된 시간을 다 쓸 때마다 우선순위를 내려 결국 다시 기반 우선순위와 같아지게 되는데,
어떠한 경우라도 동적 우선순위가 기반 우선순위보다는 더 낮아지지 않는다.
3. 쓰레드간 동기화
멀티쓰레드는 개요에서 말했듯이 한 프로세스를 여러 역할에 따라 여러 개의 쓰레드로 나뉘어 작업하는 방식이므로 각 쓰레드간의 동기화가 필요하다.
동시에 복수개의 코드가 같은 주소영역에서 실행됨으로써 서로 간섭하고 영향을 주는 경우가 빈번하기 때문이다.
멀티쓰레드의 가장 큰 문제점은 공유자원(주로 메모리의 전역변수)을 보호하기가 어렵다는 점이다.
그리고 쓰레드간의 실행순서를 제어하는 것도 쉽지 않은 문제이다.
이런 여러가지 문제점을 해결하기 위하여 쓰레드간의 실행 순서를 제어할 수 있는 여러가지 방법들을 동기화라고 한다.
동기화 방법에는, Interlocked, 임계영역, 뮤텍스, 세마포어, 이벤트등의 기법을 사용한다.
1) 임계영역 (Critical Section)
동기화문제를 해결하는 방법들 중 가장 쉬운반면 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.
임계영역(Critical Section)이란 공유자원의 독점을 보장하는 코드의 영역을 가리킨다. 이는 아래 두 함수로 시작하고 끝낸다.
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
CRITICAL_SECTION형의 포인터형은 복수개의 쓰레드가 참조해야 하므로 반드시 전역변수로 선언해야한다. 사용법은 다음과 같다.
CRITICAL_SECTION crit1, crit2;
함수 {
…
EnterCriticalSection(&crit1);
//공유자원1을 액서스한다.
LeaveCriticalSection(&crit1);
EnterCriticalSection(&crit2);
//공유자원2을 액서스한다.
LeaveCriticalSection(&crit2);
…
}
주의할것은 가급적 임계영역 내부의 코드가 빨리 끝날 수 있도록 짧은 시간을 사용하도록 작성해야 한다.
만약 Leave를 호출하지않고 쓰레드를 빠져나와버리면 이후부터는 다른 쓰레드는 이 임계영역에 들어갈 수 없게된다.
만약 이부분에서 예외가 발생하여 Leave함수가 호출되지 못하게 될 수도 있다.
그래서 임계영역을 쓸 때는 반드시 구조적 예외 처리구문에 포함시켜주는 것이 좋다.
Try {
EnterCriticalSection(&crit);
…
}
finally {
LeaveCriticalSection(&crit);
}
이렇게하면 설사 예외가 발생하더라도 Leave함수는 반드시 호출되므로 훨씬 안전해진다.
다음은 MFC 에서의 사용 예이다.
CCriticalSection g_critical; // 전역 변수로 선언
function()
{
AfxBeginThread(ThreadFuncA, NULL);
AfxBeginThread(ThreadFuncB, this);
}
UINT ThreadFuncA(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncA가 할 일....
g_critical.Unlock();
}
return 0;
}
UINT ThreadFuncB(LPVOID pParam)
{
while(1)
{
g_critical.Lock();
// ThreadFuncB가 할 일....
g_critical.Unlock();
}
return 0;
}
2) 뮤텍스(Mutex)
임계영역은 앞서 말했듯 동일한 프로세스 내에서만 사용할 수 있다.
그러나, 뮤텍스(Mutex; Mutual Exclusion;상호배제)는 임계영역이 사용된 곳에 대신 사용될 수 있으며, 프로세스 간에도 사용할 수 있다.
뮤텍스를 사용하려면 다음 함수로 생성해야 한다.
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL blInitialOwner, LPCTSTR lpName);
lpMutexAttributes : 보안속성. 대개 NULL
blInitialOwner : 뮤텍스 생성과 동시에 소유할 것인지 지정.
lpName: 뮤텍스의 이름을 지정하는 문자열.
뮤텍스는 프로세스간의 동기화에도 사용되므로 이름이 필요하고, 이 이름은 프로세스간 뮤텍스를 공유할 때 사용된다.
뮤텍스 소유를 해지하여 다른 쓰레드가 이것을 가질 수 있도록 하려면 임계영역의 LeaveCriticalSection 에 해당하는 다음 함수를 호출하면 된다.
BOOL ReleaseMutex(HANDLE hMutex);
만일 프로세스가 다른 프로세스의 쓰레드에 의해서 이미 생성된 뮤텍스의 핸들을 얻기를 원하거나,
뮤텍스가 존재하지 않는 경우에 뮤텍스를 생성하기 원한다면 다음 함수를 사용한다.
HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
3) 세마포어 (Semaphore)
세마포어도 뮤텍스와 유사한 동기화 객체이나 다른점은, 뮤텍스는 하나의 공유자원을 보호하기 위해 사용하지만,
세마포어는 제한된 일정 개수를 가지는 자원(HW, 윈도우, 프로세스, 쓰레드, 권한, 상태 등 컴퓨터에서의 모든 자원)을 보호하고 관리한다.
세마포어는 사용 가능한 자원의 개수를 카운트하는 동기화 객체이다.
세마포어와 관련된 함수는 다음과 같다.
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG IlInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
HANDLE OpenSemaphore(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount);
4) 이벤트 (Event)
임계영역, 뮤텍스, 세마포어는 주로 공유자원을 보호하기 위해 사용되는 데 비해
이벤트는 이보다는 스레드간의 작업순서나 시기를 조정하기 위해 사용한다.
특정한 조건이 만족될 때까지 대기해야 하는 쓰레드가 있을 경우 이 쓰레드의 실행을 이벤트로 제어할 수 있다.
이벤트는 자동리셋과 수동리셋이 있다.
+자동 리셋 이벤트 : 대기상태가 종료되면 자동으로 비신호상태가 된다.
+수동 리셋 이벤트 : 쓰레드가 비신호상태로 만들어줄 때까지 신호상태를 유지한다.
++신호상태 (Signaled): 쓰레드 실행가능상태. 신호상태의 동기화 객체를 가진 쓰레드는 계속 실행할 수 있다.
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
HANDLE OpenEvent(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);
bManualReset은 이벤트가 수동리셋 이벤트(manual)인지 자동리셋 이벤트(automatic)인지 지정하는데 TRUE이면 수동리셋 이벤트가 된다.
bInitialState가 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 쓰레드가 곧바로 실행을 하도록 해준다.
이벤트도 이름(lpName)을 가지므로 프로세스간의 동기화에 사용될 수 있다.
또한 이벤트가 임계영역이나 뮤텍스와 다른점은
대기함수를 사용하지 않고도, 쓰레드에서 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다. 다음 함수를 사용한다.
BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);
SetEvent는 신호상태로 만들고 ResetEvent는 비신호상태로 만든다.
다음은 MFC 에서의 사용 예이다.
CEvent g_event; // 전역변수로 선언
FunctionA()
{
AfxBeginThread(ThreadFunc, this);
}
FunctionB()
{
g_event.SetEvent(); // Lock() 함수에서 더 이상 진행하지 못하고 잠자고 있는 쓰레드를 깨워서 일을 시키려면 SetEvent()를 호출.
}
// ThreadFunc() 함수는 이벤트가 발생할 때마다 while문을 한번씩 실행.
UINT ThreadFunc(LPVOID pParam)
{
while(1)
{
g_event.Lock(); // SetEvent()가 호출되면, Lock()함수에서 실행이 중단된 쓰레드가 다음 코드를 실행.
// ThreadFunc가 할 일....
g_event.Unlock();
}
return 0;
}
'프로그래밍 > MFC' 카테고리의 다른 글
WIN32 API프로그래밍에서 MDI에서 자식 생성. (0) | 2013.08.14 |
---|---|
MFC 디렉토리 생성 (0) | 2013.08.14 |
쓰레드 생성. (0) | 2013.08.14 |
유니코드 (0) | 2013.08.14 |
CEvent 클래스 (0) | 2013.08.14 |