Windows 스레드 생성과 소멸
이 글은 윤성우 님의 '뇌를 자극하는 윈도우즈 시스템 프로그래밍'을 참고하여 공부한 내용입니다.
Windows 시스템 프로그래밍을 공부하며 생각한 적이 있습니다. Windows의 많은 매크로, 비트 플래그, typedef, C 스타일 등으로 작성된 코드를 보며 "그냥 ISO C++ 표준을 사용해서 개발하면 되는 것 아닐까?"라는 생각을 말이죠. 하지만 핸들과 커널 오브젝트 개념을 알게 되면서 Windows의 기능을 모두 사용하려면 알아둬야 하는 내용이구나 다시 깨달았죠. 맞습니다. 열심히 해야죠.
CreateThread, _beginthreadex
Windows에서 스레드를 생성하기 위해서는 위 함수들을 사용해야 합니다. 우선 CreateThread부터 알아보겠습니다.
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
- lpThreadAttributes: 보안 속성입니다. SECURITY_ATTRIBUTES 구조체를 사용하여 상속여부를 결정합니다.
- dwStackSize: 스택 메모리의 크기입니다. 0을 넣게 되면 기본 크기가 설정됩니다.
- lpStartAddress: 스레드에서 실행할 함수가 들어갑니다. 이 함수가 스레드의 시작주소가 됩니다.
- lpParameter: 스레드에 전달할 변수입니다.
- dwCreationFlags: 스레드의 생성 옵션입니다. 0을 넣게 되면 생성 후 즉시 실행됩니다.
- lpThreadId: 스레드의 식별자를 받을 정수형 변수의 주소가 들어갑니다. NULL로 넣을 경우 반환받지 않습니다.
스레드에 전달되는 lpStartAddress 함수의 형식은 아래와 같습니다.
DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
);
반환 값은 생성한 스레드의 핸들을 반환합니다. 만약 생성에 실패한다면 NULL을 반환합니다 GetLastError를 통해 에러 내용을 확인할 수 있습니다. Windows에서 간단하게 스레드를 생성할 수 있게 해 줍니다. 간단한 연산이나 C/C++ 런타임 라이브러리를 사용하지 않는 동작을 실행할 때 CreateThread를 사용할 수 있습니다.
C/C++ 런타임 라이브러리를 사용하지 못하는 이유는 CreateThread 함수에서는 C/C++ 런타임 라이브러리 관련 내용을 초기화하지 않기 때문입니다. rand와 strtok, errno, malloc, new 등의 C/C++ 런타임 함수들은 전역/정적 변수를 사용하여 런타임에서 발생하는 요청들을 처리합니다. 이때 멀티 스레드를 사용하게 되면 데이터 경합이 발생하게 되며 의도치 못한 동작이 발생할 수 있습니다. 이때 CreateThread 함수는 이런 함수들의 사용에 대한 준비작업을 하지 않습니다.
그래서 C/C++ 런타임 라이브러리를 사용하기 위해서는 _beginthreadex 함수를 사용하여 자동으로 초기화를 할 수 있습니다. 이 함수의 사용법은 CreateThread와 동일합니다.
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
매개 변수에 대한 설명은 CreateThread와 동일합니다. 단지 Windows 자료형을 사용하지 않고 표준 자료형을 사용했다는 것입니다. 그리고 가장 큰 차이점 점은 스레드를 위한 독립적인 메모리 블록을 힙에 할당한다는 점입니다. 이 메모리 블록을 TLS, Thread Local Storage라 부르며 이 TLS에 런타임 라이브러리에서 사용되는 데이터를 저장함으로써 다른 스레드들과 독립적인 런타임 환경을 제공받을 수 있는 것입니다.
_beginthreadex는 스레드가 종료되면 런타임 라이브러리를 정리하고 힙에 할당했던 메모리를 해제합니다. 이 과정을 통해 스레드에 메모리 누수 같은 문제를 발생시키지 않고 정상적으로 스레드가 종료될 수 있게 합니다. 만약 스레드 내부에서 return 이외에 방법으로 스레드를 종료해야 한다면 _endthreadex 함수를 호출해야 합니다. 이 함수는 위에서 말한 스레드 정리 과정을 포함하여 스레드를 정상적으로 종료시킵니다.
Windows Thread States
스레드의 상태는 크게 Ready, Running, Waiting으로 나눌 수 있습니다.
- Ready: 스케줄러에 의해 언제든 실행될 수 있는 상태입니다
- Running: 스레드가 CPU에서 실행되고 있는 상태입니다.
- Waiting: 동기화 오브젝트 혹은 Sleep, WaitForSingleObject 등의 Blocking 함수를 사용했을 때 기다리는 상태입니다.
나머지 상태는 커널 내부에서 사용하는 상태입니다. Standby의 경우 실행되기 직전 스레드가 대기되는 상태를 뜻하며 실행되기 전 단 하나의 스레드만 이 상태에 해당됩니다. Transition은 실행 준비는 되었지만 메모리 부족으로 대기 중인 상태입니다. Terminated는 스레드가 종료된 상태입니다. 프로세스에서 해당 스레드 핸들을 가지고 있는 경우 커널 오브젝트에 스레드의 상태를 나타내기 위해 사용됩니다.
Thread Priority Level
스레드도 스케줄링 우선순위가 존재합니다. 스레드의 우선순위는 프로세스의 우선순위에서 상대적인 순위를 의미합니다. 기본적으로 프로세스의 우선순위 클래스의 종류는 아래와 같습니다.
프로세스 우선 순위 | 의미 |
IDLE_PRIORITY_CLASS, 0x00000040 | 매우 낮음 |
BELOW_NORMAL_PRIORITY_CLASS, 0x00004000 | 조금 낮음 |
NORMAL_PRIORITY_CLASS, 0x00000020 | 기본값 |
ABOVE_NORMAL_PRIORITY_CLASS, 0x00008000 | 조금 높음 |
HIGH_PRIORITY_CLASS, 0x00000080 | 높음 |
REALTIME_PRIORITY_CLASS, 0x00000100 | 매우 높음 |
스레드의 우선순위는 위 프로세스의 우선순위에서 값을 더하거나 빼 상대적으로 결정됩니다.
스레드 우선 순위 | 값 | 의미 |
THREAD_PRIORITY_IDLE | -15 | 매우 낮음 |
THREAD_PRIORITY_LOWEST | -2 | 낮음 |
THREAD_PRIORITY_BELOW_NORMAL | -1 | 조금 낮음 |
THREAD_PRIORITY_NORMAL | 0 | 기본 |
THREAD_PRIORITY_ABOVE_NORMAL | 1 | 조금 높음 |
THREAD_PRIORITY_HIGHEST | 2 | 높음 |
THREAD_PRIORITY_TIME_CRITICAL | 15 | 매우 높음 |
그래서 실제 스레드의 우선순위는 프로세스 클래스에 스레드의 상대 우선순위를 더한 값입니다. SetThreadPriority 함수를 사용하여 스레드의 우선순위를 설정할 수 있습니다.