쓰레드 함수에 전달할 인자를 지정하는 용도로 사용된다. 매개변수 lpStartAddress가 가리키는 함수를 호출할 경우, 전달할 인자를 지정한다.
main 함수에서 argv로 문자열이 전달되는 것과 유사하다.
dwCreationFlags
쓰레드의 생성 및 실행을 조절하기 위해 사용되는 전달인자다. 인자로 CREATE_SUSPENDED가 전달되면, 쓰레드는 생성과 동시에 BLOCKED 상태에 놓인다. 그러나 함수 ResumeThread가 호출되면 실행을 시작한다.
Windows XP 이상에서는 STACK_SIZE_PARAM_IS_A_RESERVATION를 전달할 수 있는데, 이 경우는 dwStackSize를 통해 전달되는 값의 크기는 reserve 메모리 크기를 의미하게 되고, 아닐 경우 commit 메모리 크기를 의미한다.
lpThreadId
쓰레드 ID를 전달받기 위한 변수의 주소값을 전달한다. 필요가 없을 경우 NULL로 전달하면 된다. 단, Windows ME 이하 버전에서는 NULL을 전달할 수 없다.
(참고로 Windows ME는 Dos 버전의 OS다.)
위 CreateThread 함수는 Windows에서 사용할 수 있는 가장 기본적인 쓰레드 생성 함수이며, 이에 대해서 알아봤다. 그렇다면 쓰레드의 최대 생성 개수는 얼마나 될까? 이는 Windows에서 별도로 명시적으로 제한한 것이 없다. 메모리가 허락하는 정도까지 생성할 수 있다.
위 코드는 생성 가능한 쓰레드의 개수 측정하는 동작을 실행한다. 자세한 설명을 하기 전에 다음의 그림을 보고 넘어가자.
위 그림은 쓰레드의 생성과 흐름의 진행 과정을 보여준다. main thread는 프로세스 전체를 대표한다. main thread의 return문은 프로세스 종료로 이어진다. 그렇다면, main thread의 return은 모든 thread의 return을 의미하는가? 이는 아니다. thread가 작업을 마치기도 전에 사라진다. 그렇기 때문에 return을 통한 쓰레드 종료를 권하고 있는데 이는 이후 서술한다.
다시 위의 실행 결과를 보자. MAXIMUM THREAD NUMBER : 1600이 맨 마지막에 실행되지 않았다. 코드의 마지막에 선언하더라도 항상 마지막 호출을 보장한다는 것이 아님을 알 수 있다.
이러한 사유는 쓰레드 생성이 먼저 되는지, printf 함수가 먼저 호출이 되는지 알 수 없기 때문이다. 막연하게 쓰레드 생성이 시스템 리소스를 더 많이 필요로 하기 때문에 printf 함수가 먼저 호출될 것을 예측할 수 있지만, 어디까지나 예측일 뿐이다.
위 코드에서는 CreateThread 함수에서 쓰레드에 스택을 할당하는 공간에 0을 넣음으로써, Default 값인 1MB를 할당했다. 이를 1024x1024x10 바이트로 설정하면 쓰레드의 수가 줄어든다. 쓰레드 생성을 운영체제가 모두 프로그래머에게 맡기지 않고, 상황에 따라서 적절하게 조절을 하기 때문이다.
01. B. 쓰레드의 소멸
앞서 이상적인 쓰레드의 소멸은 return문을 통해 종료 및 소멸시키는 방법이라고 했다. 물론 상황에 따라서 이상적인 쓰레드 소멸 방법은 바뀔 수 있다. 여기에서 return문을 통한 종료 및 소멸이 이상적이라는 사유는 이 방법 외의 것들이 필요한 상황을 연출하기 쉽지 않기 때문이다.
Case 1 : 쓰레드 종료 시 return을 이용하면 좋은 경우
간단한 예시로 1부터 10까지 더하는 작업을 하려고 한다. 이 일은 외부에서 입·출력이 많이 필요하므로 Blocked 상태에 자주 놓일 것이 뻔하다. 일을 빨리 끝내기 위해서 세 개의 쓰레드를 생성해서 시간 감소와 성능 향상을 도모할 수 있다.
쓰레드를 통해서 정해진 시간에 CPU에게 많은 일을 시킬 수 있고 Blocked 상태에 놓이는 것도 쓰레드가 나누어 감당하기 때문이다. 다음의 예제를 보자.
위 예제는 1 ~ 10까지의 수를 더하는 동작을 실행하며, 실행 결과 하단의 GetExitCodeThread 함수는 쓰레드의 종료 코드를 얻는 함수다.
Case 2 : 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우(특정 위치에서 쓰레드의 실행을 종료시킬 경우)
VOID ExitThread (
DWORD dwExitCode
);
매개변수 명
설명
dwExitCode
커널 오브젝트에 등록되는 쓰레드 종료 코드를 지정한다.
위에는 ExitThread 함수의 원형이다. 이 함수는 실행 중인 쓰레드를 종료하고자 할 때 호출하는 함수로, return 방식의 쓰레드 종료만큼이나 선호된다. 이 함수는 return문에 의한 종료와 달리, 언제 어디에서나 쓰레드를 종료시킬 수 있다는 장점이 있다.
return문의 경우에는 main thread의 return까지 접근을 해야지 다른 쓰레드의 종료가 되었다고 볼 수 있는데 만약에 다른 함수가 호출된 상황에 쓰레드 종료가 필요하다면 Case 2에서 설명하는 방법이 유용하다.
하지만 C++을 이용한 프로그래밍 시에는 만약 A, B, C 함수가 존재하고 A 함수 -> B 함수 -> C 함수 순으로 호출되고 C 함수의 쓰레드를 종료한다면 문제가 생길 수 있다. A, B 함수의 스택 프레임에 존재하는 객체의 소멸자가 호출되지 않아 메모리 유출 현상이 발생할 수 있기 때문이다.
Case 3 : 쓰레드 종료 시 TerminateThread 함수 호출이 유용한 경우(외부에서 쓰레드 종료 시)
main 함수 내에서 쓰레드 생성 시, 해당 쓰레드의 핸들을 얻을 수 있다. 이 핸들을 통해 쓰레드를 강제 종료시킬 수 있다. 사용 시, 종료 대상이 되는 쓰레드는 외부에서 종료시키는 것이기 때문에 자신의 종료 시점을 알 수 없다. 그렇기 때문에 메모리 혹은 할당받은 리소스 해제를 처리하지 못하고 종료된다.
02. 쓰레드의 성격과 특성
이번에는 프로세스와 다른, 쓰레드의 특성이 무엇이고 주의사항들이 무엇이 있는지 보자.
02. A. 힙, 데이터, 영역, 그리고 코드 영역의 공유에 대한 검증
쓰레드는 데이터 영역과 힙 영역, 등을 공유한다. 그렇기 때문에 앞서 실습한 예제와 달리, 간결하게 개선시킬 수 있다. 앞서 예제에서는 total 변수를 지역 변수로 선언하여 각 쓰레드의 스택 영역에서 저장될 수 있게 하였으나, 이번 예제에서는 static을 사용해서 전역 변수로의 변경을 도모해 개선할 수 있다.
02. B. 동시 접근에 있어서의 문제점
쓰레드의 변경은 프로그램 라인 단위로 이루어지지 않는다. 그렇기 때문에 printf, scanf뿐만 아니라, 증감/감소 연산자 실행 중에도 실행 중인 쓰레드의 변경에 의한 컨텍스트 스위칭은 빈번하게 일어나기 때문에 둘 이상의 같은 메모리 영역을 동시에 참조하는 것은 값이 변질될 문제가 생긴다.
02. C. 프로세스로부터의 쓰레드 분리
쓰레드는 프로세스와 동일하게 생성과 동시에 Usage Count가 2가 된다. 하나는 쓰레드 종료 시 감소하고 나머지는 쓰레드 핸들을 인자로 CloseHandle 함수를 호출할 때 감소한다. 그렇기 때문에 자식 프로세스의 커널 오브젝트 소멸과 관련된 문제가 동일하게 쓰레드에서 발생할 수 있다.
실수를 막기 위해, 쓰레드 생성 시 반환된 핸들값을 인자로 전달하며 CloseHandle 함수를 바로 실행한다. 이렇게 되면 Usage Count는 1이 되고, 쓰레드의 종료와 동시에 Usage Count는 0이 되어 모든 메모리를 반환한다. 이러한 함수 호출 방법을 가리켜, 프로세스로부터 쓰레드를 분리한다고 한다.
03. 쓰레드의 상태 컨트롤
쓰레드의 상태는 프로그램 실행 과정에서 수도 없이 변경된다. 하지만, 프로그래머가 직접 쓰레드의 상태를 컨트롤해야 하는 일이 생길 수 도 있다. 이러한 경우에 대해서 알아보자.
03. A. 쓰레드의 상태 변화
다시 한 번 언급하지만, Windows에서 상태 변화가 일어나는 주체는 프로세스가 아닌, 쓰레드다. 그리고 프로세스 상태 변화와 쓰레드 상태 변화는 다를 것이 없으며, 이전에 프로세스의 상태 변화를 소개하고자 올린 다음 그림과 같다.
위 예제와 그 다음에 설명되고 있는 함수들은 쓰레드를 Blocked 상태와 Ready 상태로 지정하는 방법과 필요로 하는 함수들에 대해서 설명하고 있다. 쓰레드의 커널 오브젝트에는 SuspendThread 함수의 호출 빈도수를 기록하기 위한 서스펜드 카운트Suspend Count라고 불리는 멤버가 있다.
실행 중인 쓰레드의 호출 카운트는 0인데, 쓰레드 핸들 인자를 가지고 SuspendThread 함수를 호출한다면 서스펜드 카운트는 1이 되며, Blocked 상태로 변경된다.
04. 쓰레드의 우선순위 컨트롤
위 표는 프로세스의 우선순위를 가리킨다. 그리고 쓰레드는 다음과 같이 상대적 우선순위를 갖고 있다.
Priority
Meaning
THREAD_PRIORITY_LOWEST
-2
THREAD_PRIORITY_BELOW_NORMAL
-1
THREAD_PRIORITY_NORMAL
0 - Default
THREAD_PRIORITY_ABOVE_NORMAL
+1
THREAD_PRIORITY_HIGHEST
+2
쓰레드의 우선순위는 프로세스의 기준 우선순위, 쓰레드의 상대적 우선순위의 조합으로 결정된다. 예를 들어 기준 우선순위가 NORMAL_PRIOIRTY_CLASS(=9)인 프로세스 안에 두 개의 쓰레드가 존재하는데 각각 상대적 우선순위가 THREAD_PRIORITY_LOWEST(= -2)와 THREAD_PRIORITY_NORMAL(=0)이면, 쓰레드의 최종 순위는 아래와 같다.
7 ( 9 - 2 ), 9 ( 9 - 0 ). 이를 통해 쓰레드의 실질적인 우선순위를 계산할 수 있다.
BOOL SetThreadPriority (
HANDLE hThread,
int nPriority
);
int GetThreadPriority (
HANDLE hThread
);