본문 바로가기

High Level Technique/Window System

Thread 생성과 소멸

Thread 생성과 소멸


Windows에서 쓰레드를 생성하는 함수는 CreateThread()이다. 


쓰레드의 최대 개수는 메모리가 허용하는 만큼 가능하다. 쓰레드 또한 독립된 스택을 사용하기 때문에 이러한 스택을 만들기 위해 메모리가 허용하는 만큼가능하다.



CountThread.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
#define MAX_THREAD (1024*10)
 
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD threadNum = (DWORD)lpParam;
 
    while (1)
    {
        printf("Thread num: %d \n", threadNum);
        Sleep(5000);
    }
 
    return 0;
}
 
DWORD cntOfThread = 0;
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[MAX_THREAD];
    HANDLE hThread[MAX_THREAD];
 
    while (1)
    {
        hThread[cntOfThread] = CreateThread(NULL0, ThreadProc, (LPVOID)cntOfThread, 0&dwThreadID[cntOfThread]);
 
        if (hThread[cntOfThread] == NULL)
        {
            printf("MAX Thread Num: %d \n", cntOfThread);
            break;
        }
        cntOfThread++;
    }
 
    for (int i = 0; i < cntOfThread; i++)
    {
        CloseHandle(hThread[i]);
    }
    return 0;
}
cs




책에서는 Thread의 최대 개수가 2024로 나와있다 하지만 실제 실행해본 결과 1609가 최대 쓰레드 개수로 나왔다. 혹시나 하고 메모리 용량이 부족할까봐 늘려서 해봤지만 늘렸더니 1608로  되었다.


2024개 인것은 User 영역 크기가 2G 정도 된다고 생각할 수 있다. 하지만 실제 실행결과를 봐서는 1.6G가 정도된다. Windows 10 Ent 기준이다.


스택의 크기를 줄여 쓰레드의 개수를 늘려도 최대 쓰레드의 개수는 같게 나온다.


운영체제에서 너무 작은 크기의 스택을 요구하면 기본적으로 1M 바이트로 지정해 버리기 때문이다.




쓰레드 소멸


쓰레드 함수 내에서 return 문을 통해서 종료 및 소멸시키는 방법이 가장 이상적이라고 한다. 무조건적으로 그런건 아니지만 return 문을 통해 하는 것이 좋다고 한다.


1. return을 사용하는 경우


1부터 10까지 더하는 작업을 한다고 할 때 외부로부터 입출력이 많이(가정) 요구되기 때문에 Blocked 상태가 자주 발생할 것이다. 따라서 3개의 쓰레드가 나눠 속도를 높인다고 하자.


A 쓰레드는 1~3 B 쓰레드는 4~7 C 쓰레드는 8~10 을 더한다고 하자.


앞수와 뒷수를 넘겨주면 해당 사이의 값만 더하면 되기 때문에 하나의 쓰레드 함수를 만들면 된다.



ThreadAdderOne.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD* nPtr = (DWORD*)lpParam;
 
    DWORD numOne = *nPtr;
    DWORD numTwo = *(nPtr + 1);
 
    DWORD total = 0;
    for (int i = numOne; i <= numTwo; i++)
    {
        total += i;
    }
 
    return total;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[3];
    HANDLE hThread[3];
 
    DWORD paramThread[] = { 1347810 };
    DWORD total = 0;
    DWORD result = 0;
 
    hThread[0= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[0]), 0&dwThreadID[0]);
    hThread[1= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[2]), 0&dwThreadID[1]);
    hThread[2= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[4]), 0&dwThreadID[2]);
 
    if (hThread[0== NULL || hThread[1== NULL || hThread[2== NULL)
    {
        printf("Thread Creation Fault! \n");
        return -1;
    }
 
    WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
 
    GetExitCodeThread(hThread[0], &result);
    total += result;
 
    GetExitCodeThread(hThread[1], &result);
    total += result;
 
    GetExitCodeThread(hThread[2], &result);
    total += result;
 
    printf("total 1~10: %d \n", total);
 
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
    CloseHandle(hThread[2]);
    return 0;
}
cs



정상적으로 1부터 10까지 더해진 것을 볼 수 있다.



2.ExitThread 함수 호출이 유용한 경우


쓰레드 함수에서 A -> B -> C 로 이동하는 함수가 있다고 할 때 C에서 종료가 되어야 한다면 ExitThread()로 종료시키면 된다.

하지만 return 으로 종료시켜야 한다면 쓰레드 함수까지 리턴을 모두 해주어야 한다.


하지만 C++와 같은 객체지향적 언어에서는 C 에서 ExitThread()가 실행된다면 남아있는 A와 B의 남아있는 객체들은 소멸하지 못하고 남아있게 되어 메모리 릭이 일어날 수 있다.




3. TerminateThread() 호출이 유용한 경우


이는 외부에서 쓰레드를 종료시키고자 하는 경우이다.


메인 함수 내에서 쓰레드를 생성할 경우 해당 쓰레드의 핸들을 얻게 된다. 이 핸들을 이용해서 쓰레드를 강제 종료할 수 있다. 이 함수의 문제는 강제종료라는 것이다.




힙, 데이터 영역, 코드 영역 공유 확인


위 ThreadAdderOne.cpp를 개선해 ThreadAdderTwo.cpp를 작성해 보자.


ThreadAdderTwo.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
static int total = 0;
 
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD* nPtr = (DWORD*)lpParam;
 
    DWORD numOne = *nPtr;
    DWORD numTwo = *(nPtr + 1);
 
    for (int i = numOne; i <= numTwo; i++)
    {
        total += i;
    }
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[3];
    HANDLE hThread[3];
 
    DWORD paramThread[] = { 1347810 };
 
    hThread[0= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[0]), 0&dwThreadID[0]);
    hThread[1= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[2]), 0&dwThreadID[1]);
    hThread[2= CreateThread(NULL0, ThreadProc, (LPVOID)(&paramThread[4]), 0&dwThreadID[2]);
 
    if (hThread[0== NULL || hThread[1== NULL || hThread[2== NULL)
    {
        printf("Thread Creation Fault! \n");
        return -1;
    }
 
    WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
 
    printf("total 1~10: %d \n", total);
 
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
    CloseHandle(hThread[2]);
    return 0;
}
cs



위 코드의 문제는 total이라는 변수에 Thread가 동시에 접근한다는 것이다. 


A 쓰레드가 1부터 3까지 더하는 중에 B 쓰레드가 실행되어 4부터 7까지 더한다고 하자. 그럼 A 쓰레드는 다 더하지도 못한채 진행이 된다. 이러한 것 때문에 같은 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 높다.



ANSI 표준 Thread


ANSI 표준으로 사용하기 위해서 프로젝트 설정 - Code Generation - Runtime Library 에서 Multi-Thread로 시작해야 한다.


그리고 CreateThread() 대신에 _beginthreadex를 사용해야 한다. _beginthreadex 함수도 내부적으로 쓰레드 생성을 위해서 CreatThread를 호출하지만 독립적인 메모리 블록을 할당한다.




쓰레드의 상태 컨트롤


쓰레드의 상태는 프로그램이 실행되는 과정에서 수도 없이 변경된다. 운영체제의 관리 방법에 따라 바뀌게 되는데 프로그래머가 상태를 직접적으로 컨트롤 해야 하는 경우도 있다. 특정 쓰레드를 지목하여 지목한 쓰레드의 실행을 멈추기 위해서 Blocked 상태로 만든다거나 다시 실행을 시키기 위해서 Ready 상태로 둔다거나 하는 경우를 말한다.



처음 프로세스의 상태에 대해서 이야기 했을 때 Ready, Running, Blocked 경우가 있는데 이 과정이 모두 Thread 상태라고 말했습니다.


그럼 이제 Thread를 컨트롤 해보도록 하죠.


특정 쓰레드를 지목해서 Blocked 상태로 바꾸거나 Ready 상태로 바꿀 수 있습니다. 이 때 사용하는 함수가 SuspendThread, ResumeThread 입니다.


SuspendThread()는 Blocked 상태로 바꾸는 함수고 ResumeThread()는 Ready 상태로 두는 함수입니다. 하지만 커널 오브젝트에 SuspendThread() 호출의 빈도수를 기록하기 위한 서스 펜드 카운트라 불리는 멤버가 존재하는데 현재 실행 중인 쓰레드의 서스펜드 카운트는 0이 아닙니다. 이 쓰레드의 핸들을 인자로 suspendThread()가 호출되면 서스펜드 카운트는 1이 되고 Blocked 상태로 바뀌게 됩니다. 다시 SuspendThread()를 호출하면 2가 되죠.


여기서 Ready 상태가 되도록 ResumeThread 함수 호출을 하게 되면 Ready로 바뀌지 않는데 이 이유는 ResumeThread()가 서스펜드 카운트를 -1하기 때문에 최종적으로 서스펜드 카운트는 1이 되죠.

즉 두번 호출이 되어야 Ready 상태로 바뀌게 됩니다.



쓰레드 우선순위


저번 블로깅에서 프로세스의 우선순위에 대해서 말했었는데 이번에는 쓰레드의 우선순위 입니다.


한 프로세스에 여러개의 쓰레드를 만들 수 있기 때문에 이에 대한 우선순위를 변경하는 것입니다.




 THREAD_PRIORITY_LOWEST

 -2

 THREAD_PRIORITY_BELOW_NORMAL

 -1

 THREAD_PRIORITY_NORMAL

 0

 THREAD_PRIORITY_ABOVE_NORMAL

 1

 THREAD_PRIORITY_HIGHEST

 2


NORMAL_PRIORITY_CLASS인 프로세스 안에 두개의 쓰레드가 있다고 하면 상대적 우선순위가 THREAD_PRIORITY_LOWEST, THREAD_PRIORITY_NORMAL이 된다.


프로세스 우선순위를 기준으로 상대적 우선순위에 해당 하는 값을 빼거나 더하면 실제 쓰레드의 우선순위를 구할 수 있습니다.

'High Level Technique > Window System' 카테고리의 다른 글

생산자/소비자 모델  (1) 2017.01.04
쓰레드 동기화  (0) 2017.01.03
쓰레드 (Thread)  (0) 2017.01.02
우선순위 알고리즘  (0) 2017.01.02
프로세스 환경변수  (0) 2017.01.02