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의 저장을 위한 변수의 주소 값 전달.

복잡해 보이지만, 실제로 신경 쓸 것은 lpStartAddresslpParameter 두 가지 정도이며, 나머지는 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*)&param, 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 상태가 되기 전에는 반환하지 않는다.