XII : 쓰레드의 생성과 소멸
뇌를 자극하는 윈도우즈 시스템 프로그래밍 을 읽고 정리한 문서입니다 ;)
Windows에서의 쓰레드 생성과 소멸
CreateThread
Windows에서 사용할 수 있는 가장 기본적인 쓰레드 생성 함수는 CreateThread
이다.
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpTA,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
LPSECURITY_ATTRIBUTES lpTA
: 프로세스를 생성하는CreateProcess
와 마찬가지로, 이 또한 핸들의 상속 여부를 이 파라미터를 통해 결정할 수 있다.SIZE_T dwStackSize
: 해당 쓰레드를 위한 스택이 별도로 생성될 때, 그 크기를 지정할 수 있다. 만일 크기가 1M 보다 작다면 윈도우가 최소 1M는 보장시킬 것이다.LPTHREAD_START_ROUTINE lpStartAddress
: 쓰레드의main
역할을 할 함수를 지정하는 전달인자이다.typedef DWORD (WINAPI *PTHREAD_START_ROUTINE) (LPVOID lpThreadParameter); typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;
위처럼 지정되어 있기 때문에 반환 타입이
DWORD
이고, 매개변수 타입은LPVOID
인 형태로 함수가 정의되어야 한다.LPVOID lpParameter
: 쓰레드 함수에 전달할 인자를 지정하는 용도이다.DWORD dwCreationFlags
: 쓰레드의 생성 및 실행을 조절하기 위해 사용하는 인자이다.CREATE_SUSPENDED
가 전달되면, 생성과 동시에 Blocked 상태에 놓이게 된다.LPDWORD lpThreadId
: 쓰레드 ID를 전달받기 위한 변수의 주소값을 전달한다.
쓰레드는 유저 메모리가 허용하는 한 제한 없이 생성할 수 있지만, 쓰레드 당 스택 크기는 1M로 윈도우가 보장하듯이 동작하게 된다.
메인 쓰레드에서의 return
문은 프로세스의 종료로 이어지게 되고, 이 경우 해당 프로세스의 다른 쓰레드들 또한 같이 소멸되게 된다.
멀티 쓰레드 기반 프로그래밍을 하면서 초보 프로그래머가 범하는 과오 중에 하나가 쓰레드의 흐름을 예측하려고 하는 것이다. 시스템의 당시 상황에 따라 항상 다르게 동작할 것이므로 예측은 의미가 없다.
쓰레드의 소멸
가장 이상적인 쓰레드의 소멸 방법은, 해당 쓰레드 함수 내에서 return
문을 통해 종료 및 소멸시키는 것이다.
쓰레드가 종료 시 return
문을 통해 반환한 값은 GetExitCodeThread
함수를 통해 받아올 수 있다.
ExitThread
라는 함수를 사용한다면, 현재 실행 중인 쓰레드를 바로 종료시킬 수 있기에 return
방식의 종료만큼이나 선호되는 방식 중 하나이다.
이는 쓰레드 내부에서 깊숙히 함수 콜이 들어간 경우에 한 번의 return
으로 종료가 불가능하므로 편리함을 주지만, 본래 return
을 통해 동작해야 할 객체의 소멸자를 동작시킬 수 없다는 유의점을 알고 있어야 한다.
외부에서 TerminateThread
함수 콜을 통해 해당 쓰레드를 종료시킬 수 있지만, 이는 당연히 해당 쓰레드가 사용 중이던 리소스 해제 등을 처리하지 못하고 종료되기에 권장되지 않는다.
쓰레드의 성격과 특성
쓰레드는 메모리를 공유한다. 특히 전역변수가 할당되는 데이터 영역과, 메모리가 동적으로 할당되는 힙 영역을 공유한다.
따라서, 전역변수를 통해 모든 쓰레드가 연산을 처리한다면 코드는 매우 간결해지고 연산 또한 빨라질 것이다. 단, 이 경우 문제가 생길 수 있다.
동시접근에 있어서의 문제점
쓰레드가 동시에 실행되는 것처럼 보이지만 사실은 돌아가면서 실행되는 것인데, 왜 동시 접근이 문제가 되는가 라는 문제에 대한 답변은, 연산 과정을 상세히 살펴보면 된다.
연산이 이루어지기 위해서는 메모리에 저장된 데이터를 레지스터로 이동해야 한다. 이후 ALU에 의해 실질적인 연산이 진행되고, 이를 다시 메모리에 저장하는 구조이다.
문제는 이 일련의 동작은 원자성이 보장되지 않는다는 점이다. 레지스터에 저장되기까지만 하고 컨텍스트 스위칭이 일어날 수 있는 것이다.
때문에 둘 이상의 쓰레드가 같은 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높다.
이전에 배운 핸들 테이블은 쓰레드별로 개별된 것이 아닌, 프로세스의 소유이다. 쓰레드들 끼리는 스택 이외의 것들을 공유하기 때문이다.
쓰레드 또한 자식 프로세스와 마찬가지로 생성 시 Usage Count는 2가 된다. 따라서 해당 쓰레드가 실제로 종료되더라도 자원이 반환되지 않을 수 있다.
이러한 문제를 막기 위해, CreateThread
를 통해 쓰레드를 생성하고 나면 반환된 핸들값을 곧바로 CloseHandle
해서 문제를 방지한다.
이러한 행위를 프로세스로부터 쓰레드를 분리한다라고 표현하기도 한다.
ANSI 표준 C 라이브러리와 쓰레드
strtok()
함수의 동작을 보면, 내부적으로 문자열을 저장하여 연산하고 있는 것을 알 수 있다. 이러한 동작원리는 멀티쓰레드에서 문제를 야기시킬 수 있다.
따라서 우리는 마이크로소프트가 제공하는, 멀티쓰레드에 안전한 ANSI 표준 라이브러리를 사용해야 한다.
이는 프로젝트 설정에서 Runtime Library를 Multi-Thread가 들어간 항목으로 변경해야 한다.
또한, 쓰레드를 생성할 때 CreateThread
함수가 아닌 _beginthreadex
함수를 사용해야 한다. 이는 내부적으로 CreateThread
함수를 호출하지만, 그에 앞서 쓰레드를 위해 독립적인 메모리 블록을 할당해 준다.
Multi-로 시작하는 이름의 표준 C 라이브러리 함수는 이렇게 할당된 쓰레드 각각의 메모리 블록을 기반으로 연산을 하게 된다.
ExitThread
또한 _endthreadex
로 변경해야 하는데, 이는 앞서 할당한 독립적인 메모리 블록을 해제하기 위함이다.
만일 쓰레드에서 return
으로 종료하게 되면 자동으로 _endthreadex
함수가 호출되므로 여러모로 쓰레드의 종료는 return
문을 통하는게 좋다.
쓰레드의 상태 컨트롤
쓰레드의 상태는 프로그램이 실행되는 과정에서 수도 없이 변경된다. 하지만 경우에 따라서는 쓰레드의 상태를 직접 특정 상태로 변경시켜야 하는 경우도 있을 수 있다.
이 때 사용하는 함수가 SuspendThread
함수와, ResumeThread
함수이다.
쓰레드의 커널 오브젝트에는 이 함수들과 관련되어 Suspend Count 변수가 존재하는데, 이는 SuspendThread
가 호출될 때마다 1씩 증가하고, ResumeThread
함수가 호출될 때마다 1씩 감소하게 된다. 따라서, 중지시킨 만큼 재시작을 호출해 주어야 Blocked 상태에서 빠져나올 것이다.
쓰레드의 우선순위 컨트롤
Windows에서는 프로세스가 우선순위를 갖는 것이 아니라, 프로세스 안에서 동작하는 쓰레드가 우선순위를 갖는다.
프로세스가 가지는 우선순위를 기준 우선순위라고 표현한다. 그리고, 이에 쓰레드의 상대적 우선순위가 더해진 값이 스케줄링에 사용된다.
쓰레드의 상대적 우선순위는 다음과 같다.
Priority | Meaning |
---|---|
THREAD_PRIORITY_LOWEST | -2 |
THREAD_PRIORITY_BELOW_NORMAL | -1 |
THREAD_PRIORITY_NORMAL | 0 |
THREAD_PRIORITY_ABOVE_NORMAL | 1 |
THREAD_PRIORITY_HIGHEST | 2 |
이러한 쓰레드 상대적 우선순위는 SetThreadPriority
함수로 설정할 수 있다.
이것만은 알고 갑시다
CreateThread
함수와_beginthreadex
함수의 차이점_beginthreadex
함수를 사용해서 쓰레드를 생성할 경우 쓰레드별로 독립적인 메모리 공간을 할당받는다. 이 메모리 공간은 ANSI 표준 함수를 호출하는 과정에서 사용한다.
- 둘 이상의 쓰레드가 동시접근하는 메모리 공간의 문제점
- 우리가 보는 코드 한줄 한줄은 원자성 있게 실행되는 것이 아니고, CPU에게 전달되는 명령어 한줄 한줄이 원자성이 있는 것이므로 (일부 제외), 멀티쓰레드인 경우 동일 메모리 공간을 참조한다면 연산이 뒤틀릴 수 있게 된다.
- 쓰레드의 상태 변화
- Windows는 실행의 주체가 프로세스가 아닌 쓰레드이다. 따라서 상태를 지니는 것도 프로세스가 아닌 쓰레드이다. 쓰레드가 Ready, Running, Blocked 상태들 간 수없이 변화하며 프로그램이 실행된다.
- 프로세스로부터의 쓰레드 분리
- 프로세스의 생성과 마찬가지로 쓰레드를 생성하고 나면 생성된 쓰레드의 Usage Count가 2가 된다. 따라서 생성 시 리턴받은 핸들을 곧바로
CloseHandle
함으로서 Usage Count를 1로 만들어 커널 오브젝트가 해당 쓰레드의 생명주기를 따라가도록 한다.
- 프로세스의 생성과 마찬가지로 쓰레드를 생성하고 나면 생성된 쓰레드의 Usage Count가 2가 된다. 따라서 생성 시 리턴받은 핸들을 곧바로
'공부한 이야기 > 윈도우 OS' 카테고리의 다른 글
XIV : 쓰레드 동기화 기법 II (0) | 2023.04.29 |
---|---|
XIII : 쓰레드 동기화 기법 I (0) | 2023.04.29 |
XI : 쓰레드의 이해 (0) | 2023.04.29 |
X : 컴퓨터 구조에 대한 세 번째 이야기 (0) | 2023.04.29 |
IX : 스케줄링 알고리즘과 우선순위 (0) | 2023.04.29 |