Windows 쓰레드
Windows 쓰레드
윈도우에서의 쓰레드 생성방법
운영체제는 쓰레드의 관리를 위해서 커널 오브젝트도 함께 생성한다. 이 커널 오브젝트의 구분자 역할을 하는, 정수로 표현되는 ‘핸들(Handle)‘을 반환한다. 참고로 핸들은 리눅스의 파일 디스크립터에 비유된다.
#include <windows.h>
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadArrtibutes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
)
// 성공 시 쓰레드 핸들, 실패 시 NULL 반환
lpThreadAttributes
: 쓰레드의 보안관련 정보전달, 디폴트 보안설정을 위해서 NULL 전달.dwStackSize
: 쓰레드에게 할당할 스택의 크기를 전달, 0 전달시 디폴트 크기의 스택 생성.lpStartAddress
: 쓰레드의 main 함수정보 전달.lpParameter
: 쓰레드의 main 함수호출 시 전달할 인자정보 전달.dwCreationFlags
: 쓰레드 생성 이후의 행동을 결정, 0을 전달하면 생성과 동시에 실행 가능한 상태가 된다.lpThreadId
: 쓰레드 ID의 저장을 위한 변수의 주소 값 전달.
복잡해 보이지만, 실제로 신경 쓸 것은
lpStartAddress
와lpParameter
두 가지 정도이며, 나머지는 0 또는 NULL을 전달하면 된다.
윈도우 쓰레드의 소멸시점
윈도우 쓰레드의 소멸시점은 쓰레드에 의해서 처음 호출된 쓰레드의 main 함수가 반환하는 시점이다(이렇듯 리눅스와 소멸 시점 및 소멸 방법이 다르다). 이 방법 이외에도 쓰레드의 종료를 유도하는 방법이 있지만, 가장 좋은 방법은 쓰레드의 main 함수를 종료(반환)하는 것이다.
쓰레드에 안전한 C 표준함수의 호출을 위한 쓰레드 생성
생성된 쓰레드를 통해서 C/C++ 표준함수를 호출하려면 다음 함수를 이용해서 쓰레드를 생성해야 한다. 왜냐하면 CreateThread
함수호출을 통해서 생성되는 쓰레드는 C/C++ 표준함수에 대해서 안정적으로 동작하지 않기 때문이다.
#include <process.h>
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( *start_address ) ( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
)
// 성공 시 쓰레드 핸들, 실패 시 0 반환
_beginthreadex
함수 이전에_beginthread
함수
_beginthread
함수는 쓰레드 생성시 반환되는 핸들을 무효화시켜서 커널 오브젝트에 접근할 수 있는 방법을 막아버리는 문제점이 있다.
쓰레드 생성 예제
#include <stdio.h>
#include <windows.h>
#include <process.h> /* _beginthreadex, _endthreadex */
unsigned WINAPI ThreadFunc(void *arg);
int main(int argc, char *argv[])
{
HANDLE hThread;
unsigned threadID;
int param = 5;
hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)¶m, 0, &threadID);
if(hThread == 0)
{
puts("_beginthreadex() error");
return -1;
}
Sleep(3000);
puts("end of main");
return 0;
}
unsigned WINAPI ThreadFunc(void *arg)
{
int i;
int cnt = *((int*)arg);
for(i = 0 ; i < cnt ; i++)
{
Sleep(1000); puts("running thread");
}
return 0;
}
WINAPI
라는 윈도우 고유의 키워드는 매개변수의 전달방향, 할당된 스택의 반환방법 등을 포함하는 함수의 호출규악을 명시해 놓은 것이다._beginthreadex
함수가 요구하는 호출규약을 지키기 위해서 삽입한 것으로 이해하면 된다.- HANDLE과 달리 쓰레드 ID는 프로세스가 달라져도 중복되지 않는다는 특징이 있다.
main 함수의 반환으로 인해 프로세스가 종료되면, 그 안에 담겨있는 모든 쓰레드들도 함께 종료된다. 따라서 이에 대한 별도의 해결책이 필요하다(WaitFor~ 함수)
커널 오브젝트의 두 가지 상태
일반적으로 다음과 같은 사실에 관심을 두기 마련이다.
- “프로세스가 언제 종료되지?”
- “쓰레드가 언제 종료되지?”
때문에 우리는 다음과 같이 물을 수 있다.
- “이 프로세스 언제 종료되나요?”
- “이 쓰레드 언제 종료되나요?”
그런데 운영체제는 이러한 관심사에 대한 정보를 커널 오브젝트에 기록해준다.
- “프로세스가 쓰레드가 종료되면 해당 커널 오브젝트를
signaled
상태로 변경해 놓겠다.”
프로세스와 쓰레드의 커널 오브젝트의 초기 상태는 non-signaled
상태이다. 이는 boolean 형으로 표현이 된다.
이벤트가 발생한 상황을 TRUE로 두고, signaled
상태라 하는 것이다.
“이 커널 오브젝트는 현재 signaled
상태인가요?”
이러한 질문을 위해 정의된 두 함수가 WaitForSingleObject
, WaitForMultipleObject
이다.
WaitForSingleObject, WaitForMultipleObject
#include <windows.h>
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
// 성공 시 이벤트 정보, 실패 시 WAIT_FAILED 반환
hHandle
: 상태확인의 대상이 되는 커널 오브젝트의 핸들을 전달dwMilliseconds
: 1/1000초 단위로 타임아웃을 지정, 인자로INFINITE
전달 시, 커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않는다.- 반환 값: signaled 상태로 인한 반환 시,
WAIT_OBJECT_0
반환, 타임아웃으로 인한 반환 시WAIT_TIMEOUT
반환
위 함수는 이벤트 발생에 의해서(signaled 상태가 되어서) 반환되면, 해당 커널 오브젝트를 다시 non-signaled
상태로 되돌리기도 한다.
이렇게 다시 non-signaled
상태가 되는 커널 오브젝트를 가리켜 ‘auto-reset 모드’ 커널 오브젝트라 하고, 자동으로 non-signaled
상태가 되지 않는 커널 오브젝트를 가리켜 ‘manual-reset’커널 오브젝트라고 한다.
다음 함수는 둘 이상의 커널 오브젝트를 대상으로 상태를 확인하는 경우에 호출하는 함수이다.
#include <windows.h>
DWORD WaitForMultipleObjects(
DWORD nCount,
const HANDLE* lpHandles,
BOOL bWaitAll,
DWORD dwMilliseconds
)
// 성공 시 이벤트 정보, 실패 시 WAIT_FAILED 반환
nCount
: 검사할 커널 오브젝트의 수 전달lpHandles
: 핸들정보를 담고 있는 배열의 주소 값 전달.bWaitAll
: TRUE 전달 시, 모든 검사대상이 signaled 상태가 되어야 반환, FALSE 전달 시, 검사대상 중 하나라도 signaled 상태가 되면 반환.dwMilliseconds
: 1/1000초 단위로 타임아웃을 지정, 인자로INFINITE
전달 시, 커널 오브젝트가 signaled 상태가 되기 전에는 반환하지 않는다.