[Windows System Programming] 쓰레드의 생성과 소멸
<쓰레드의 생성과 소멸>
[공부했던 것을 되짚어보며]
https://sevenshards.tistory.com/56
[Windows System Programming] 쓰레드의 이해
[공부했던 것을 되짚어보며] 사실 지금까지 우리는 프로세스에 대한 이야기만 계속 늘어놨습니다. 커널 오브젝트에, 핸들에, 핸들 테이블과 프로세스 간 통신 이야기만 했습니다. 그리고 쓰레드
sevenshards.tistory.com
지난 글에서는 쓰레드에 대한 개념적인 이해를 했습니다.
쓰레드를 왜 쓰는지, 그리고 Windows에서의 쓰레드는 어떤 것인지에 대해서도 확인했습니다.
이번 글에서는 쓰레드를 직접 생성하고 소멸하는 과정을 거치면서 배웠던 내용을 정리해보려고 합니다.
[Windows에서의 쓰레드 생성과 소멸]
이전 글에서 다뤘던 내용은 쓰레드에 대한 기본적인 이론들입니다.
이론만 다뤘기 때문에 소스코드는 하나도 없이 글을 써내려갔습니다.
분명히 재미있는 내용은 아닙니다만, 개념을 알아야 이해를 할 수가 있습니다.
이번 내용은 이론을 실제로 구현하고 확인하는 과정이 될 것 같습니다.
[쓰레드의 생성]
CreateProcess와 마찬가지로 가장 기본적인 쓰레드 생성 함수는 CreateThread 함수입니다.
[CountThread.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: CountThread.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 생성 가능한 쓰레드의 개수를 확인하는 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#define MAX_THREADS (1024*10)
// 쓰레드에서 동작할 함수, 쓰레드의 main이 역할을 할 함수
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
DWORD threadNum = (DWORD)lpParam;
while (1)
{
_tprintf(_T("thread num: %d\n"), threadNum);
Sleep(5000);
}
return 0;
}
// 쓰레드의 개수를 기록할 전역변수
DWORD cntOfThread = 0;
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[MAX_THREADS];
HANDLE hThread[MAX_THREADS];
// 생성 가능한 최대 개수의 쓰레드 생성
while (1)
{
hThread[cntOfThread] =
CreateThread(
NULL, // 디폴트 보안(상속) 속성 지정
0, // 디폴트 스택 사이즈(1M)
ThreadProc, // 쓰레드 함수
(LPVOID)cntOfThread, // 쓰레드 함수의 전달 인자
0, // 디폴트 생성 flag 지정
&dwThreadID[cntOfThread] // 쓰레드 ID를 전달받기 위한 변수의 주소값
);
// 쓰레드 생성 확인
if (hThread[cntOfThread] == NULL) // NULL인 경우 쓰레드가 생성되지 않은 것
{
_tprintf(_T("MAXIMUM Thread Number: %d\n"), cntOfThread);
break;
}
cntOfThread++;
}
// 생성한 쓰레드의 갯수만큼 리소스 해제, Usage Count 1씩 감소
for (DWORD i = 0; i < cntOfThread; i++)
CloseHandle(hThread[i]);
return 0;
}
쓰레드 생성에 대한 간략한 예시를 코드로 작성한 것입니다.
예제에서는 10240개의 쓰레드를 생성할 수 있도록 했습니다.
실제로 쓰레드는 OS에서 최대 생성 가능한 쓰레드 수를 지정할 수도 있습니다.
그리고 지정하지 않고 메모리가 허용하는 만큼 생성할 수 있습니다.
위 예제 코드에서 쓰레드의 스택 사이즈를 변경해가면서 결과를 확인해보시는 것도 좋습니다.
[쓰레드의 소멸]
앞에서는 쓰레드를 생성하는 방법을 알아봤습니다.
이번에는 쓰레드가 소멸하는 경우에 대해서 알아보겠습니다.
엄밀히 따지면 종료가 되는 상황을 이야기하려고 합니다.
이는 크게 세 가지 경우로 나뉘어집니다.
1) 쓰레드 종료 시 return을 이용하는 경우
가장 일반적인 경우이면서 가장 이상적인 경우입니다.
쓰레드가 수행할 작업을 다 마치고 return을 통해서 종료하게 됩니다.
간단한 예로 쓰레드를 3개를 만들어서 1부터 10까지 덧셈을 하는 예제 코드입니다.
[ThreadAdderOne.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: ThreadAdderOne.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 1~10까지의 합을 3개의 쓰레드로 나눠서 수행하는 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
DWORD* nPtr = (DWORD*)lpParam;
DWORD numStart = *nPtr;
DWORD numEnd = *(nPtr + 1);
DWORD total = 0;
for (DWORD i = numStart; i <= numEnd; i++)
total += i;
return total;
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[3]; // 쓰레드의 ID를 받을 변수
HANDLE hThread[3]; // 생성한 쓰레드의 핸들을 받기 위한 변수
DWORD paramThread[] = { 1, 3, 4, 7, 8, 10 };
DWORD total = 0;
DWORD result = 0;
// 쓰레드 생성
hThread[0] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[0]),
0,
&dwThreadID[0]
);
hThread[1] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[2]),
0,
&dwThreadID[1]
);
hThread[2] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[4]),
0,
&dwThreadID[2]
);
// 세 쓰레드 중 하나라도 NULL이다 -> 핸들값이 반환되지 않음 -> 생성 실패
if (hThread[0] == NULL || hThread[1] == NULL || hThread[2] == NULL)
{
_tprintf(TEXT("Thread creation fault!\n"));
return -1;
}
// WaitForMultipleObjects -> WaitForSingleObject와 유사함
// n개의 리소스를 대상으로 커널 오브젝트가 Signaled 상태가 될 때까지 기다린다
WaitForMultipleObjects(
3, // 관찰할 커널 오브젝트의 대상의 수
hThread, // 커널 오브젝트의 배열 정보
TRUE, // 모든 커널 오브젝트가 Signaled 상태가 될 때까지 대기한다
INFINITE // 계속 기다린다
);
// 각 쓰레드에서 계산한 값을 합친다.
GetExitCodeThread(hThread[0], &result);
total += result;
GetExitCodeThread(hThread[1], &result);
total += result;
GetExitCodeThread(hThread[2], &result);
total += result;
_tprintf(TEXT("total (1~10): %d\n"), total);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
return 0;
}
2) ExitThread 함수를 통한 쓰레드의 종료
ExitThread는 쓰레드를 실행 중인 쓰레드를 종료시킬 때 사용하는 함수입니다.
return과 다르게 내가 원하는 시점에서 쓰레드를 종료시킬 수 있는 경우에 사용합니다.
이를테면 쓰레드 내에서 함수 A, B, C가 있다고 했을 때, 함수는 A→B→C 순으로 호출됩니다.
그럼 쓰레드가 return에 의해서 종료되려면 C→B→A의 역순으로 모두 return을 해야합니다.
하지만 때에 따라서는 도중에 종료를 해야할 경우도 있습니다.
그런 경우에 사용하는 함수가 ExitThread 함수입니다.
물론 주의해야 할 점이 있습니다.
C++을 기준으로 생성자 호출을 통한 객체가 있습니다.
만약 C 함수에서 ExitThread 함수가 호출된다 가정하겠습니다.
그렇게 되면 A와 B함수에서 객체를 생성한 경우 소멸자를 호출하지 않게 됩니다.
따라서 메모리 유출(Memory Leak)이 발생할 수 있으므로 주의해야합니다.
3) TerminateThread 함수를 통한 쓰레드의 종료
마지막으로는 TerminateThread 함수를 통한 쓰레드의 종료입니다.
위의 두 경우는 쓰레드 내에서 이뤄지는 종료라고 하면 이 방식은 외부에서 쓰레드를 종료하는 방식입니다.
main에서 쓰레드를 생성하면 쓰레드의 핸들을 얻게 됩니다.
그리고 이 핸들을 이용하여 쓰레드를 강제로 종료 시키는 방식입니다.
강제로 종료하기 때문에 쓰레드가 수행하고 있던 작업들은 전부 중단됩니다.
만약 메모리 할당을 하고 있는 경우에 종료가 되었다면 마찬가지로 메모리 유출이 생길 수 있습니다.
그래서 이 방법으로 종료를 하는 것은 특정 상황이 생기는 것이 아니라면 사용할 일이 손에 꼽는다고 볼 수 있습니다.
[쓰레드의 성격과 특성]
[힙, 데이터, 코드 영역의 공유에 대한 검증]
예제인 ThreadAdderOne.cpp에서는 쓰레드의 종료코드를 연산결과의 반환용으로 사용했었습니다.
그래서 return 값에 total을 줬고, total이라는 변수를 따로 줬었죠.
거기다가 GetExitCodeThread라는 함수까지 써가면서 종료코드를 얻어오는 번거로운 코드가 되었습니다.
우리가 알고 있는 쓰레드는 힙, 데이터, 코드 영역을 공유합니다.
[ThreadAdderTwo.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: ThreadAdderTwo.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 1~10까지의 합을 3개의 쓰레드로 나눠서 수행하는 예제(2) - 전역변수 기반
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
static int total = 0;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
DWORD* nPtr = (DWORD*)lpParam;
DWORD numStart = *nPtr;
DWORD numEnd = *(nPtr + 1);
for (DWORD i = numStart; i <= numEnd; i++)
total += i;
return 0; // 정상 종료
}
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[3]; // 쓰레드의 ID를 받을 변수
HANDLE hThread[3]; // 생성한 쓰레드의 핸들을 받기 위한 변수
DWORD paramThread[] = { 1, 3, 4, 7, 8, 10 };
// 쓰레드 생성
hThread[0] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[0]),
0,
&dwThreadID[0]
);
hThread[1] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[2]),
0,
&dwThreadID[1]
);
hThread[2] =
CreateThread(
NULL,
0,
ThreadProc,
(LPVOID)(¶mThread[4]),
0,
&dwThreadID[2]
);
// 세 쓰레드 중 하나라도 NULL이다 -> 핸들값이 반환되지 않음 -> 생성 실패
if (hThread[0] == NULL || hThread[1] == NULL || hThread[2] == NULL)
{
_tprintf(TEXT("Thread creation fault!\n"));
return -1;
}
// WaitForMultipleObjects -> WaitForSingleObject와 유사함
// n개의 리소스를 대상으로 커널 오브젝트가 Signaled 상태가 될 때까지 기다린다
WaitForMultipleObjects(
3, // 관찰할 커널 오브젝트의 대상의 수
hThread, // 커널 오브젝트의 배열 정보
TRUE, // 모든 커널 오브젝트가 Signaled 상태가 될 때까지 대기한다
INFINITE // 계속 기다린다
);
_tprintf(TEXT("total (1~10): %d\n"), total);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hThread[2]);
return 0;
}
이전 예제와 달리 total을 전역 변수로 선언하여 코드를 변경하였습니다.
보다시피 코드의 양이 줄었고, 쓰레드의 반환 값을 다른 용도로 사용하는 것도 가능해졌습니다.
GetExitCodeThread와 같은 시스템 함수의 호출도 줄었기 때문에 훨씬 간결해진 코드가 되었습니다.
그리고 사실 ThreadAdderOne.cpp부터 코드 영역이 공유되고 있었다는 사실도 어느 정도 인지는 하고 있으셨나요?
쓰레드는 서로 다 다르지만 쓰레드의 main이 되는 함수(ThreadProc)는 다 공유를 하고 있었습니다.
즉 위 예제는 쓰레드가 코드와 데이터 영역을 공유한다는 것을 확인할 수 있는 코드입니다.
그런데 이 코드는 사실 문제가 있습니다.
[동시 접근에 있어서의 문제점]
위의 예제를 실행하면서 이상한 결과를 보신 분은 없으셨을 겁니다.
아무래도 1부터 10까지의 덧셈을 나눠서만 하는 것이기 때문에 큰 문제는 없었을 것이라고 봅니다.
그런데 이 예제의 코드에는 확실하게 문제가 있습니다.
바로 '동시 접근'에 대한 것입니다.
(강의 자료가 없어서 그림의 화질이 좋지 않습니다.책이 있으신 분들은 책을 참고하면 좋을 것 같습니다.)
실제로 쓰레드는 돌아가면서 아주 빠르게 실행을 합니다.
그래서 '동시에 접근'하는 것 처럼 보이는 문제가 생기게 되는데 위의 그림을 토대로 이해를 해보겠습니다.
우선 total에는 10이라는 값이 있는 상태고, A와 B라는 두 개의 쓰레드가 있습니다.
쓰레드 A는 6을 더하려고 하고 쓰레드 B는 9를 더하려고 하는 상황입니다.
그렇게 되면 우리가 생각하는 결과값은 25가 나와야 정상입니다.
먼저 쓰레드 A가 6을 더하기 위해서 다음과 같이 값을 가져와 레지스터에 더한 값을 저장하는 상황입니다.
16이라는 값을 레지스터에 저장했으니 남은 것은 메모리에 이 값을 저장(STORE)하기만 하면 됩니다.
그런데 이 때! 스케줄러에 의해서 쓰레드 A와 쓰레드 B의 컨텍스트 스위칭이 발생하게 됩니다.
어쩔 수 없이 쓰레드 A는 컨텍스트 스위칭에 의해서 계산했던 값을 메모리에 저장하게 됩니다.
그리고 쓰레드 B는 계산을 수행합니다.
여기서부터 일이 틀어지기 시작하는 것이죠.
total에는 16이 들어가있어야 마땅할 것을 10이라는 값을 가져오게 됩니다.
설상가상이라고, 이제 쓰레드 B의 연산 결과가 저장이 되어버립니다.
이제 쓰레드 B의 연산이 끝났기 때문에 19라는 값을 저장하고 종료하게 됩니다.
이제 이어서 쓰레드 A가 다시 저장한 데이터를 복원해서 이전 작업을 수행하게 됩니다.
이제 마지막으로는 19가 저장되어 있는 값에 16을 저장하는 불상사가 일어납니다.
앞서 25라는 결과값을 기대했지만, 실제로는 16이라는 값을 얻게 됩니다.
프로세스 때도 그랬고, 쓰레드 때에도 마찬가지입니다.
컨텍스트 스위칭은 실행 중인 프로그램의 라인 단위로 이뤄지지 않습니다.
그래서 scanf로 데이터를 읽어들이는 중이나 printf로 데이터를 출력하는 중에도 얼마던지 일어날 수 있습니다.
단순하게 ++나 --와 같이 증감 연산을 하는 도중에도 컨텍스트 스위칭은 일어나게 됩니다.
이처럼 둘 이상의 쓰레드가 같은 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높습니다.
그리고 이를 해결하기 위해서는 '동기화'라는 개념을 이용하게 됩니다.
[프로세스로부터의 쓰레드 분리]
프로세스는 쓰레드를 담는 상자라고 했었습니다.
그래서 실제로 Windows에서는 프로세스가 아닌 쓰레드를 주로 스케줄링을 한다고 했었죠.
'쓰레드가 핸들 테이블을 가지고 있는 것 아닐까' 하는 생각을 하실 수도 있습니다.
하지만 여전히 프로세스가 핸들 테이블을 가지게 됩니다.
그리고 하나의 프로세스에는 하나의 핸들 테이블이 존재합니다.
여기서 개념을 복습하는 차원에서 되짚고 넘어가보기 위해 문제를 하나 내볼까 합니다.
"프로세스 A가 특정 리소스 C(파이프, 메일슬롯, 파일 등등)를 생성하면서 얻은 핸들값이 204다.
그런데 프로세스 A와는 아무런 관계도 없는 프로세스 B에서 이 리소스 C의 핸들값을 알고 있다.
그렇다면 이 204라는 정보만으로 리소스 C에 접근이 가능한가?"
https://sevenshards.tistory.com/47
[Windows System Programming] 프로세스간 통신(IPC) - (1)
[이번 글을 따라서 진행하기에 앞서] 여기까지 와서 글을 보고 공부하시는 분은 뭐... 얼마 없을거라고 생각합니다. 하지만 복습하는 차원에서 글을 읽는 분들에게 설명하는 것이 가장 좋은 학습
sevenshards.tistory.com
https://sevenshards.tistory.com/48
[Windows System Programming] 프로세스간 통신(IPC) - (2)
[이번 글을 따라 진행하기에 앞서] 이번 글의 제목대로 지난 글에 이어서 IPC에 대한 이야기를 마저 하려고 합니다. 그런데 그 전에 또 이야기를 좀 해두고 갈까 합니다. 아마 아시는 분들은 아시
sevenshards.tistory.com
'가능하다'라고 하신 분들은 이 부분을 다시 공부하고 오시는 것을 권장드립니다.
핸들값은 핸들 테이블에 정보가 등록이 된 이후에 이 핸들 테이블의 소유자인 프로세스에게 의미가 있게 됩니다.
즉, 핸들값이 핸들 테이블에 등록이 되어있지 않다면 접근을 할 수 없습니다.
알고 있어봐야 아무런 쓸모가 없다는 것입니다.
이제 이걸 쓰레드의 관점에서 좀 풀어가보겠습니다.
프로세스 A 내에서 생성된 쓰레드는 이 204라는 핸들값에 의미가 있을까요?
있습니다! 그리고 접근도 가능합니다.
다시 말해서 프로세스 내에서 생성된 모든 쓰레드들은 프로세스의 핸들 테이블을 공유하기 때문입니다.
쓰레드는 핸들 테이블까지 공유한다는 것입니다.
이제 본론으로 돌아와서, Usage Count 이야기로 넘어가보겠습니다.
프로세스가 자식 프로세스를 생성할 때, 자식 프로세스의 Usage Count는 2가 됩니다.
그리고 이 Usage Count가 줄어드는 상황이 하나는 자식 프로세스가 종료되었을 때입니다.
또 하나는 부모 프로세스에서 CloseHandle 함수를 호출할 때입니다.
전에 프로세스와 쓰레드는 생성 시 Usage Count는 2라고 했습니다.
그 이외의 커널 오브젝트의 Usage Count는 1이라고 했습니다.
쓰레드 역시 프로세스와 마찬가지로 Usage Count와 관련된 커널 오브젝트 소멸의 문제가 동일하게 발생합니다.
그래서 종료코드를 필요로 하지 않는 경우는 생성과 동시에 CloseHandle 함수 호출로 Usage Count를 1 감소 시킵니다.
쓰레드가 종료되면 자연스레 Usage Count는 0이 되면서 커널 오브젝트도 소멸하게 되기 때문입니다.
이를 가리켜 "프로세스로부터 쓰레드를 분리한다" 라고 표현합니다.
[ANSI 표준 C라이브러리와 쓰레드]
우리가 지금까지 C언어를 배우면서 가장 처음 접하게 되는 함수는 아마 scanf와 printf였을 겁니다.
그리고 이 함수들은 표준 C 라이브러리를 사용합니다.
이처럼 문자열 처리와 입/출력에 관련된 것들은 표준 C 라이브러리의 의존도가 높습니다.
초기에는 표준 C 라이브러리가 구현될 당시에 쓰레드에 대한 고려가 이뤄지지 않았었습니다.
그래서 우리가 지금까지 작성했던 코드는 표준 C 라이브러리를 사용하지 않았습니다.
그렇기 때문에 동시접근이 발생할 수 있는 문제가 있습니다.
가장 대표적인 예로는 ANSI C 표준 함수인 strtok 함수를 사용하는 예제를 들어볼까 합니다.
[strtok.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: strtok.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 문자열을 토큰으로 나누는 strtok_s 사용 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <string.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
// 문자열 선언 시 다음과 같이 이어서 작성해도 하나의 문자열이 된다.
TCHAR string[] =
TEXT("Hey, get a life!")
TEXT("You don't even have two pennies to rub together.");
// strtok_s에 의해서 분리된 후의 남은 문자열이 들어가는 변수
TCHAR* context = NULL;
// 토큰(token)으로 쪼개기 위한 기준(구분자, Separator)을 정의.
TCHAR seps[] = TEXT(" ,.!");
// 토큰 분리 조건, 문자열 설정 및 첫 번째 토큰 반환
// 그래서 첫 token에는 Hey가 들어간다.
TCHAR* token = _tcstok_s(string, seps, &context);
// 반복문을 통해 토큰을 계속 출력
while(token != NULL)
{
_tprintf(TEXT("%s\n"), token);
token = _tcstok_s(NULL, seps, &context);
}
return 0;
}
보다시피 코드 내용 자체는 되게 단순합니다.
문자열을 공백(' '), 쉼표(','), 마침표('.'), 느낌표('!')를 기준으로 문자열을 토큰 단위로 나누는 함수입니다.
그런데 여기서 생각해봐야 할 것은 이 함수를 처음 호출 할 때의 문자열은 어딘가에 저장이 된다는 것입니다.
그래서 while문에서는 NULL을 전달하면서 더 이상 분해할 토큰이 없을 때까지 수행하는 것이고요.
문자열은 전역 또는 static으로 선언된 배열, 즉 데이터 영역에 저장이 되어있을 것입니다.
그런데 이걸 멀티 쓰레드 기반으로 구현하게 되면 동시 참조(접근) 문제가 발생하게 됩니다.
그렇다면 이를 해결할 방법은 무엇일까요?
다행히도 MS에서는 멀티 쓰레드에 안전한 ANSI 표준 라이브러리를 제공하고 있습니다.
아마 여러분이 사용하고 있는 VS에서는 기본적으로 이걸 지원하고 있습니다.
그리고 추가적으로 이제 CreateThread 함수가 아닌 _beginthreadex라는 함수를 사용합니다.
ExitThread의 경우에는 _endthreadex함수가 됩니다.
_beginthreadex 함수의 경우 내부적으로는 쓰레드 생성을 위해 CreateThread 함수를 호출합니다.
그리고 추가적으로 쓰레드마다 별도의 메모리 공간을 할당하게 됩니다.
마찬가지로 _endthreadex 함수 역시 내부적으로는 ExitThread 함수를 호출합니다.
추가적으로 쓰레드마다 별도로 할당된 메모리 공간을 해제하는 역할도 수행하게 됩니다.
그래서 앞으로는 멀티 쓰레드 환경에서 안전한 런타임 라이브러리인 _beginthreadex 함수를 쓰시는 것이 좋습니다.
이후 예제 코드에서도 전부 쓰레드 생성 시에는 이 함수를 사용하게 될 것입니다.
실제로 CreateThread 함수와 인자 부분에서도 일부 매개변수나 반환값이 달라지는 것 이외에는 큰 차이는 없습니다.
그래서 일부 형변환을 해주거나 쓰레드 함수 선언에 변경이 일부 가해지는 부분 외에는 없습니다.
쓰시다보면 익숙해지게 됩니다.
아래의 예제 코드는 CreateThread를 _beginthreadex로 바꾼 것입니다.
[CountThreadMultiThread.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: CountThreadMultiThread.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 멀티 쓰레드 기반에 안전한 런타임 라이브러리 함수 사용의 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <process.h>
#include <Windows.h>
#define MAX_THREADS (1024*10)
// 쓰레드에서 동작할 함수, 쓰레드의 main이 역할을 할 함수
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
DWORD threadNum = (DWORD)lpParam;
while (1)
{
_tprintf(_T("thread num: %d\n"), threadNum);
Sleep(5000);
}
return 0;
}
// 쓰레드의 개수를 기록할 전역변수
DWORD cntOfThread = 0;
int _tmain(int argc, TCHAR* argv[])
{
DWORD dwThreadID[MAX_THREADS];
HANDLE hThread[MAX_THREADS];
// 생성 가능한 최대 개수의 쓰레드 생성
while (1)
{
hThread[cntOfThread] =
(HANDLE)_beginthreadex(
NULL, // 디폴트 보안(상속) 속성 지정
0, // 디폴트 스택 사이즈(1M)
ThreadProc, // 쓰레드 함수
(LPVOID)cntOfThread, // 쓰레드 함수의 전달 인자
0, // 디폴트 생성 flag 지정
(unsigned*) & dwThreadID[cntOfThread] // 쓰레드 ID를 전달받기 위한 변수의 주소값
);
// 쓰레드 생성 확인
if (hThread[cntOfThread] == NULL) // NULL인 경우 쓰레드가 생성되지 않은 것
{
_tprintf(_T("MAXIMUM Thread Number: %d\n"), cntOfThread);
break;
}
cntOfThread++;
}
// 생성한 쓰레드의 갯수만큼 리소스 해제, Usage Count 1씩 감소
for (DWORD i = 0; i < cntOfThread; i++)
CloseHandle(hThread[i]);
return 0;
}
[쓰레드의 상태 제어]
사실 쓰레드의 상태는 프로세스와 큰 차이가 없습니다.
이전에도 말했다시피 쓰레드도 Ready, Running, Blocked 상태를 가지게 됩니다.
[쓰레드의 상태 변화]
프로세스 때와 마찬가지이므로 자세한 설명은 하지 않겠습니다.
[Suspend & Resume]
이제 쓰레드의 상태를 제어해보는 함수들에 대해서 소개해보겠습니다.
Running 상태에서 Blocked 상태로 이동시킬 때 사용하는 함수는 SuspendThread라는 함수입니다.
반대로 Blocked 상태에서 Ready 상태로 이동시킬 때 사용하는 함수는 ResumeThread입니다.
다만 여기서는 좀 독특한 개념이 있습니다.
쓰레드의 커널 오브젝트에는 SuspendThread 함수의 호출 빈도 수를 기록하기 위한 Suspend Count라는 것이 있습니다.
그래서 SuspendThread가 1회 호출되면 카운트가 1씩 증가하게 됩니다.
반대로 ResumeThread 함수는 카운트를 1 감소 시키는 역할을 수행합니다.
Suspend Count가 0이 되면 쓰레드는 다시 Ready 상태로 돌아가게 됩니다.
만약 Suspend Count가 2라면, ResumeThread 함수를 2회 호출해야 합니다.
SuspendThread와 ResumeThread 두 함수를 사용한 예제 코드입니다.
[SuspendCount.cpp]
/*
* Windows System Programming - 쓰레드의 생성과 소멸
* 파일명: SuspendCount.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-07
* 이전 버전 작성 일자:
* 버전 내용: 쓰레드의 상태 변경 확인 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <process.h>
#include <Windows.h>
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
while (1)
{
_tprintf(TEXT("Running State!\n"));
Sleep(10000);
}
return 0;
}
int main(int argc, TCHAR* argv[])
{
DWORD dwThreadID;
HANDLE hThread;
DWORD susCnt; // suspend count를 확인하기 위한 변수
hThread = (HANDLE)_beginthreadex(
NULL,
0,
ThreadProc,
NULL,
CREATE_SUSPENDED, // 쓰레드 생성 시 Blocked 상태로 시작, Suspend Count는 1
(unsigned*)&dwThreadID
);
// 쓰레드 생성 확인
if (hThread == NULL)
_tprintf(TEXT("Thread creation failed!\n"));
// Tm
susCnt = ResumeThread(hThread); // 쓰레드를 다시 Ready 상태로, Suspend Count는 0
_tprintf(TEXT("suspend count: %d\n"), susCnt);
Sleep(10000);
susCnt = SuspendThread(hThread); // 쓰레드를 다시 Blocked 상태로, Suspend Count는 1
_tprintf(TEXT("suspend count: %d\n"), susCnt);
Sleep(10000);
susCnt = SuspendThread(hThread); // 쓰레드를 Blocked 상태에서 Suspend Count를 추가, Suspend Count는 2
_tprintf(TEXT("suspend count: %d\n"), susCnt);
Sleep(10000);
susCnt = ResumeThread(hThread); // Blocked 상태의 쓰레드의 Suspend Count를 1감소, Suspend Count는 1
_tprintf(TEXT("suspend count: %d\n"), susCnt);
Sleep(10000);
susCnt = ResumeThread(hThread); // Blocked 상태의 쓰레드의 Suspend Count를 1감소, Suspend Count는 0이 되므로 쓰레드는 다시 Ready 상태
_tprintf(TEXT("suspend count: %d\n"), susCnt);
Sleep(10000);
// 프로세스가 종료되는 것을 막기 위해 쓰레드의 실행을 확인한다
WaitForSingleObject(hThread, INFINITE);
return 0;
}
[쓰레드의 우선순위 제어]
이전부터 꽤 반복해서 했던 말이지만, Windows에서 실행의 주체는 프로세스가 아닌 쓰레드입니다.
그래서 실제로 우선순위도 쓰레드가 갖게 되는 것이죠.
그럼 프로세스의 우선 순위는 아무런 필요도 없는 것이 아닌가 싶으실겁니다.
꼭 그렇지만은 않습니다.
프로세스가 가진 우선순위는 '기준' 우선 순위입니다.
다시 말해서 이걸 기본으로 깔고 들어가는 것입니다.
그리고 쓰레드가 가지는 우선순위는 '상대적' 우선 순위입니다.
쓰레드의 실질적인 우선 순위는 '기준 우선순위(프로세스) + 상대 우선순위(쓰레드)'가 됩니다.
쓰레드의 우선 순위를 변경하는 데 사용하는 함수는 SetThreadPriority 함수입니다.
그리고 쓰레드의 우선순위를 확인하는 함수는 GetThreadPriority 함수입니다.