Event

 

이벤트란 어떤 사건이 일어났음을 알리는 동기화 객체이다.
크리티컬 섹션, 뮤텍스, 세마포어는 주로 공유 자원을 보호하기 위해 사용되고 이벤트는 스레드 간의 작업 순서나 시기를 조정하고

신호를 보내기 위해 사용된다.

특정한 조건이 만족될 때까지 대기해야 하는 스레드가 있을 경우 이 스레드의 실행을 이벤트로 제어할 수 있다.

 

이벤트를 기다리는 스레드는 이벤트가 신호상태가 될 때까지 대기하며 신호상태가 되면 대기를 풀고 작업을 시작한다.

이때 대기 함수가 대기를 풀면서 이벤트 객체를 어떻게 처리하는가에 따라 두 가지 종류로 구분된다.


 - 자동 리셋 이벤트 : 대기 상태가 종료되면 자동으로 비신호상태가 된다.
 - 수동 리셋 이벤트 : 스레드가 비신호상태로 만들 때까지 신호를 유지한다.

 

하기 두 함수는 이벤트를 만들거나 오픈하는 함수이다.

 

<CreateEvent>

1
2
3
4
5
6
7
8
HANDLE
WINAPI
CreateEvent(
    __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
    __in     BOOL bManualReset,
    __in     BOOL bInitialState,
    __in_opt LPCTSTR lpName
    );
cs

 

<OpenEvent>

1
2
3
4
5
6
7
HANDLE
WINAPI
OpenEvent(
    __in DWORD dwDesiredAccess,
    __in BOOL bInheritHandle,
    __in LPCTSTR lpName
    );
cs

 

bManualReset 설정 값이 TRUE이면 수동 리셋 이벤트이고 FALSE이면 자동 리셋 이벤트가 된다.
bInitialState 설정 값이 TRUE이면 이벤트를 생성함과 동시에 신호상태로 만들어 이벤트를 기다리는 스레드가 곧바로 실행된다.
이벤트도 이름을 가지므로 프로세스간의 동기화에 사용될 수 있다.
이벤트가 동기화 객체와 또 다른 점은 대기 함수를 사용하지 않고도 임의적으로 신호상태와 비신호상태를 설정할 수 있다는 점이다.

이때는 다음 함수를 사용한다.

 

1
2
3
BOOL WINAPI SetEvent(__in HANDLE hEvent);
 
BOOL WINAPI ResetEvent(__in HANDLE hEvent);
cs

 

SetEvent는 이벤트를 신호상태로 만들고 ResetEvent는 이벤트를 비신호상태로 만든다.

 

 

Auto Reset Event (자동 리셋 이벤트)

 

멀티 스레드가 진가를 발휘할 때는 백그라운드 작업을 할 때이다.

예를 들어 백그라운드에서는 시간이 걸리는 계산, 인쇄, 다운로드, 자료 정렬 등을 할 때 스레드에게 분담할 수 있다.

 

우선 수만건의 DB 내용을 읽는 과정을 메인 스레드에서 진행하면 어떻게 되는지 확인해보자.

하기 예는 메인 스레드에서 DB 내용을 읽는 과정을 처리하는 코드이다.

 

1
2
3
4
5
6
7
8
9
void DBRead(void)
{
    int i;
    for (i=0; i<1000000++i)
    {
        Sleep(30); //시간이 오래 걸리는 작업임을 나타내기 위한 Sleep.
        buf[i] = i;
    }
}
cs

 

Sleep 함수가 시간이 오래 걸리는 과정이라 생각하고 보면, 메인 스레드는 DBRead 함수에서 DB 내용을 모두 읽을 때까지

기다려야만 한다. 즉, DB Read 과정 동안 메인 스레드는 아무 처리를 하지 못하게 되므로 이런 함수는 하기와 같이 별도의

스레드로 분리해야 한다.

 

1
2
3
4
5
6
7
8
9
DWORD WINAPI ThreadFunc(LPVOID prc)
{
    int i;
    for (i=0; i<1000000++i)
    {
        Sleep(30); //시간이 오래 걸리는 작업임을 나타내기 위한 Sleep.
        buf[i] = i;
    }
}
cs

 

이제 이 스레드는 새로 스택을 생성하고 자신만의 실행 흐름을 가지며 메인 스레드와는 따로 스케줄링된다.

만약 이 계산 작업이 메인 스레드와는 아무런 상관이 없는 단순 백그라운드 작업이라면 아무 문제가 없지만 메인 스레드에

작업 결과를 전달해주어야 한다면 이는 또 다른 문제의 발생 원인이다. 그렇게 되면 결국 메인 스레드는 Read 동작이 완료될

때까지 기다린 후 DB 내용을 화면에 표시해주어야 한다. 결국 메인 스레드에서 함수를 호출한 것과 다를게 하나도 없게 되버린다.


그러나 생각을 바꾸면 모든 데이터를 읽을 때까지 주 스레드가 기다릴 필요없이 당장 출력할 정도만 되면 일단 화면에 표시하고

남은 작업은 백그라운드 스레드가 하도록 맡겨 놓으면 이런 문제를 해결할 수 있다.

 

이런 처리를 하려면 작업 스레드가 어디까지 계산을 했는지 주 스레드에게 알릴 필요가 있으며 이런 통신에 이벤트가 사용된다.

다음 예제는 30개의 DB 내용을 Read한 후에 화면에 출력을 하는 코드이다.

 

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
58
59
60
HANDLE hEvent;
int buf[100];
 
DWORD WINAPI ThreadFunc(LPVOID prc)
{
    int i;
    for (i=0; i<1000000++i)
    {
        Sleep(30);
        buf[i] = i;
        if (i == 30)
            SetEvent(hEvent); //hEvent를 신호상태로 변경.
    }
 
    return 0;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    int i;
    TCHAR str[128];
    HDC hdc;
    PAINTSTRUCT ps;
    DWORD ThreadID;
    HANDLE hThread;
    TCHAR *Mes = "마우스 왼쪽 버튼을 눌러 주십시오.";
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //비신호상태 자동 리셋 이벤트 생성.
        return TRUE;
    case WM_LBUTTONDOWN:
        InvalidateRect(hWnd, NULL, TRUE);
        UpdateWindow(hWnd);
        hThread = CreateThread(NULL0, ThreadFunc, NULL0&ThreadID);
        CloseHandle(hThread);
        WaitForSingleObject(hEvent, INFINITE); //hEvent가 신호상태가 될 때까지 대기.
                                               //ThreadFunc에서 hEvent를 신호상태로 변경시키면 하기 코드 실행.
        hdc = GetDC(hWnd);
        for (i=0; i<=30++i)
        {
            wsprintf(str, "%d Line is %d", i, buf[i]);
            TextOut(hdc, 10, i*20, str, lstrlen(str));
        }
        ReleaseDC(hWnd, hdc);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        TextOut(hdc, 100100, Mes, strlen(Mes));
        EndPaint(hWnd, &ps);
        return 0;
    case WM_DESTROY:
        CloseHandle(hEvent);
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

WM_CREATE에서 최초 비신호상태인 자동 리셋 이벤트를 만들었고 사용자가 마우스 왼쪽 버튼을 클릭하면 별도의 스레드에서 DB를

읽기 시작한다. 그리고 DB 항목 중 30개를 읽으면 SetEvent를 통해서 hEvent를 신호 상태로 변경하여 메인 스레드에서 DB 내용을

출력하도록 한다.

WaitingForSingleObject는 hEvent가 신호 상태가 될 때까지 대기하다가 신호 상태가 되면 하기 코드를 실행시켜준다.

 

별도의 스레드에서는 SetEvent 후 자신이 맡은 작업을 백그라운드에서 혼자 진행하면 되고 메인 스레드는 자신의 다른 작업을 계속

이어서 하면 된다.

 

그리고 대기 함수인 WaitingForSingleObject 함수는 일단 대기가 종료되면 다음 사용을 위해 hEvent를 다시 비신호 상태로 변경시킨다.

 

 

동기화 객체.

 

동기화 객체란 말 그대로 동기화에 사용되는 객체이다. 프로세스, 스레드처럼 커널 객체이며 프로세스 한정적인 핸들을 가진다.

앞에서 살펴본 크리티컬 섹션은 구조체일 뿐이지 커널 객체는 아니다.

 

동기화 객체는 크리티컬 섹션보다 느리기는 하지만 훨씬 더 복잡한 동기화에 사용될 수 있다.

동기화 객체는 일정 시점에서 다음 두 가지 상태 중 한 상태를 가진다.

 

 - 신호 상태 : 스레드의 실행을 허가하는 상태. 신호상태의 동기화 객체를 가진 스레드는 계속 실행할 수 있다.

                  신호등의 파란불에 비유할 수 있다.

 

 - 비신호 상태 : 스레드의 실행을 허가하지 않는 상태. 신호상태가 될 때까지 스레드는 블록된다.

                     신호등의 빨간불에 비유할 수 있다.

 

동기화 객체는 대기 함수와 함께 사용되는데 대기 함수(Wait Function)는 일정한 조건에 따라 스레드의 실행을 블록하거나

실행을 허가하는 함수이다. 여기서 일정한 조건이란 주로 동기화 객체의 신호 여부가 된다.

 

 <대표적인 대기 함수>

1
DWORD WINAPI WaitForSingleObject(__in HANDLE hHandle, __in DWORD dwMilliseconds);
cs

  

 이 함수는 hHandle이 지정하는 하나의 동기화 객체가 신호상태가 되기를 기다린다.

 dwMilliseconds 인수는 타임 아웃 시간을 1/1000초 단위로 지정하는데 이 시간이 경과하면 설사 동기화 객체가

 비신호상태이더라도 즉시 리턴함으로써 무한 대기를 방지한다. 대신 dwMilliseconds를 INFINITE로 지정하면

 신호상태가 될 때까지 무한정 대기한다.

 

 WaitForSingleObject 함수는 동기화 객체가 신호상태가 되거나 타임 아웃 시간이 경과할 때까지 스레드의 실행을

 블록하는 역할을 한다고 할 수 있다.

 이 함수의 리턴값을 검사해 보면 어떤 이유로 대기상태를 종료했는지를 알 수 있다.

 

 - WAIT_OBJECT_0 : hHandle 객체가 신호상태가 되었다.

 - WAIT_TIMEOUT : 타임 아웃 시간이 경과하였다.

 - WAIT_ABANDONED : 포기된 뮤텍스.

 

 WaitForSingleObject 함수는 리턴하기 전에 hHandle 동기화 객체의 상태를 변경한다. (신호상태 → 비신호상태)

 그래서 일단 한 스레드가 동기화 객체를 소유하면 동기화 객체는 비신호상태가 되므로 다른 스레드가 이 객체를

 중복하여 소유하지 못한다.

 

1) 뮤텍스.

 

뮤텍스는 크리티컬 섹션과 여러 가지 면에서 비슷하며 크리티컬 섹션이 쓰이는 곳에 대신 사용될 수 있다.

프로세스간에도 사용할 수 있다는 장점이 있으나 그만큼 속도는 느리다. 그러나 같은 프로세스내의 스레드끼리만

동기화한다면 속도 차이는 크리티컬 섹션과 거의 없다.

 

Mutex는 Mutual Exclusion의 준말인데 두 스레드가 동시에 소유할 수 없다는 뜻이며 오직 한 스레드에 의해서만

소유될 수 있으며 어떤 스레드에게 소유되면 뮤텍스는 비신호상태가 된다.

반대로 어떠한 스레드에서도 소유하지 않고 있으면 신호상태인 것이다.

 

1
2
HANDLE WINAPI CreateMutex(__in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, __in BOOL bInitialOwner,
    __in_opt LPCSTR lpName);
cs

 

우선 CreateMutex함수를 통해서 Mutext를 생성 한 후 그 핸들을 리턴받는다.

 

 - 첫 번째 인수는 보안속성을 지정하는데 대개 NULL로 준다.

 

 - 두 번째 인수는 뮤텍스를 생성함과 동시에 소유할 것인지를 지정하는데 이 값이 TRUE이면 이 함수를 호출한 스레드가 먼저

   뮤텍스를 소유한 상태에서 동작한다. FALSE인 경우는 최초로 대기 함수를 호출하는 함수가 이 뮤텍스를 소유하는데 스레드에서

   뮤텍스를 생성할 때는 통상 FALSE로 준다.

 

 - 세 번째 인수는 뮤텍스의 이름을 지정하는 문자열이다. 프로세스 간에 뮤텍스가 공유될 때 이 이름을 이용한다.

   커널 객체들은 이런 식으로 문자열로 된 이름을 가짐으로써 공유가 가능하며 다른 프로세스에서 뮤텍스의 이름만 알면 OpenMutex

   함수로 뮤텍스의 핸들을 얻을 수 있다. 또는 같은 이름으로 CreateMutex를 한 번 더 호출해도 된다.

 

1
HANDLE WINAPI OpenMutex(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in LPCSTR lpName);
cs

 

CreateMutex, OpenMutex 함수가 지정하는 뮤텍스의 이름은 시스템 전역적으로 유일하게 하나의 뮤텍스를 가리킨다.

(즉, 1 PC에 동일한 이름의 서로 다른 뮤텍스는 존재할 수 없다는 것임.)

만약 한 프로세스 내에서만 사용한다면 뮤텍스의 이름을 줄 필요없이 세 번째 인자에 NULL을 주면된다.

 

뮤텍스를 파괴할 때는 모든 커널 객체와 마찬가지로 CloseHandle 함수를 사용한다.

CloseHandle은 뮤텍스 핸들을 닫으며 만약 이 핸들이 대상 뮤텍스를 가리키는 마지막 핸들이라면 뮤텍스 객체도 파괴된다.

뮤텍스는 스스로 카운트를 관리하며 모든 핸들이 닫힐 때 객체도 소멸된다. (스마트 포인터랑 같은 개념이라고 생각하면 됨.)

 

또한 비신호 상태의 뮤텍스를 다시 신호 상태로 변경할 때는 하기 함수를 사용한다.

1
BOOL WINAPI ReleaseMutex(__in HANDLE hMutex);
cs

 

<뮤텍스 사용 예제>

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
58
59
60
61
62
63
64
65
66
int X;
HANDLE hMutex;
 
DWORD WINAPI ThreadFunc1(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        WaitForSingleObject(hMutex, INFINITE); //Mutex가 신호 상태가 될 때가지 무한정 대기.
                                               //Mutex가 신호 상태가 되었다면 대기함수를 지나고
                                               //Mutex는 비신호 상태로 변경된다.
        X = 100;
        Sleep(1);
        TextOut(hdc, X, 100"고양이"6);
        ReleaseMutex(hMutex);                  //대기 함수가 변경한 뮤텍스의 비신호 상태를
                                               //다른 Thread의 대기 함수 또한 실행될 수 있도록
                                               //신호상태로 변경한다.
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
DWORD WINAPI ThreadFunc2(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        WaitForSingleObject(hMutex, INFINITE); //Mutex가 신호 상태가 될 때가지 무한정 대기.
        X = 200;
        Sleep(1);
        TextOut(hdc, X, 200"강아지"6);
        ReleaseMutex(hMutex);                  //신호 상태로 변경.
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HANDLE hThread;
    DWORD ThreadID;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hMutex = CreateMutex(NULL, FALSE, NULL); //Mutex 생성.
        hWndMain = hWnd;
        return 0;
    case WM_LBUTTONDOWN:
        hThread = CreateThread(NULL0, ThreadFunc1, NULL0&ThreadID);
        CloseHandle(hThread);
        hThread = CreateThread(NULL0, ThreadFunc2, NULL0&ThreadID);
        CloseHandle(hThread);
        return 0;
    case WM_DESTROY:
        CloseHandle(hMutex); //Mutex 파괴.
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

 

멀티 스레드의 문제점

 

멀티 스레드의 가장 큰 문제점은 공유 자원을 보호하기 어렵다는 점이다. 공유 자원은 주로 메모리 영역의 전역 변수인

경우가 대부분이다. 동일한 프로세스에 속한 스레드는 같은 주소 공간에서 실행되며 전역변수를 공유하므로 문제의

소지가 많다. 두 스레드가 같은 변수에 값을 대입하는 경우, 앞쪽 스레드가 대입해 놓은 값은 뒤쪽 스레드가 대입한 값에

의해 지워진다.

이와 같이 스레드가 공유 자원을 서로 사용하려는 상태를 경쟁 상태(race condition)이라 한다.

 

또한 경쟁 상태를 해소하기 위해서는 실행 순서를 통제해야 하는데 그러다 보면 최악의 경우 스레드끼리 서로를 기다리는

교착상태(deadlock)가 발생하기도 한다.

 

이러한 여러 가지 문제를 해결하기 위해 스레드간의 실행 순서를 제어할 수 있는 기술들을 동기화(Synchronization)이라 한다.

 

우선 예제를 보자.

 

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
int X;
 
DWORD WINAPI ThreadFunc1(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        X = 100;
        Sleep(1);
        TextOut(hdc, X, 100"고양이"6);
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
DWORD WINAPI ThreadFunc2(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        X = 200;
        Sleep(1);
        TextOut(hdc, X200"강아지"6);
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HANDLE hThread;
    DWORD ThreadID;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        return 0;
    case WM_LBUTTONDOWN:
        hThread = CreateThread(NULL0, ThreadFunc1, NULL0&ThreadID);
        CloseHandle(hThread);
        hThread = CreateThread(NULL0, ThreadFunc2, NULL0&ThreadID);
        CloseHandle(hThread);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

위의 소스는 X라는 값을 ThreadFunc1과 ThreadFunc2에서 동시에 접근하고 있다.

그러므로 Thread1에서는 X값이 100이어야 하고 Thread2에서는 X 값이 200이어야 하는데 결과를 보면 전혀 그렇지 않다.

두 Thread 모두 X 값이 100일 때도 있고 200일 때도 있게 된다.

 

이유를 설명하기 위해 예를 들자면...

10번 line 소스에서 스위칭이 발생하면 ThreadFunc1이 동작하는 중에 ThreadFunc2가 동작하게 된다.

그럼 X값은 100이었지만 ThreadFunc2를 거치면서 X값은 200으로 바뀌게 된다.

그리고 다시 스위칭이 발생하면 ThreadFunc1의 동작이 이어서 진행이 되는데, 원래 Thraed1의 X는 100이어야하는데

Thread2를 거치면서 200이라는 값으로 바뀌었기 때문에 원하지 않은 200이라는 값을 가지고 Thread1에서 처리하게 되는

것이다.

 

반대의 경우도 마찬가지이기 때문에 실행 결과의 Text가 찍히는 좌표는 사용자가 원하던 좌표가 아니게 되는 것이다.

 

해결 방법

 

1) 크리티컬 섹션

 

 가장 이해하기 쉽고 속도도 빠른 방법이지만 동일한 프로세스 내에서만 사용해야 하는 제약이 있다.

 다른 스레드에의해 방해받지 말아야 할 작업을 할 때 이 영역을 크리티컬 섹션으로 둘러싸 놓으면 전역 자원의 독점권이

 주어진다.

 

 <크리티컬 섹션 주요 함수>

1
2
3
4
5
6
7
VOID WINAPI InitializeCriticalSection(__out LPCRITICAL_SECTION lpCriticalSection);
VOID WINAPI DeleteCriticalSection(__inout LPCRITICAL_SECTION lpCriticalSection);
 
 
VOID WINAPI EnterCriticalSection(__inout LPCRITICAL_SECTION lpCriticalSection);
VOID WINAPI LeaveCriticalSection(__inout LPCRITICAL_SECTION lpCriticalSection);
 
cs

 

 모든 인자는 CRITICAL_SECTION 형의 포인터를 인수로 요구하므로 CRITICAL_SECTION 형 변수의 주소를 전달하면 된다.

 이 변수는 복수 개의 스레드가 참조해야 하므로 반드시 전역변수로 선언해야 한다.

 

 전역 변수 선언 후 InitializeCriticalSection 함수로 초기화하면 이후부터 크리티컬 섹션을 사용할 수 있다.

 다 사용한 후에는 DeleteCriticalSection으로 파괴해야 하며 EnterCriticalSection 함수와 LeaveCriticalSection 함수를 가지고

 보호될 코드를 감싼다. 아래 예제를 보면 사용법에 대해 쉽게 알 수 있다.

 

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
58
59
60
61
62
int X;
CRITICAL_SECTION crit;
 
DWORD WINAPI ThreadFunc1(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        EnterCriticalSection(&crit); //크리티컬 섹션 보호 시작
        X = 100;
        Sleep(1);
        TextOut(hdc, X, 100"고양이"6);
        LeaveCriticalSection(&crit); //크리티컬 섹션 보호 종료.
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
DWORD WINAPI ThreadFunc2(LPVOID temp)
{
    HDC hdc;
    hdc = GetDC(hWndMain);
    for (int i=0; i<100++i)
    {
        EnterCriticalSection(&crit); //크리티컬 섹션 보호 시작
        X = 200;
        Sleep(1);
        TextOut(hdc, X, 200"강아지"6);
        LeaveCriticalSection(&crit); //크리티컬 섹션 보호 종료.
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HANDLE hThread;
    DWORD ThreadID;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        InitializeCriticalSection(&crit); //크리티컬 섹션 초기화.
        hWndMain = hWnd;
        return 0;
    case WM_LBUTTONDOWN:
        hThread = CreateThread(NULL0, ThreadFunc1, NULL0&ThreadID);
        CloseHandle(hThread);
        hThread = CreateThread(NULL0, ThreadFunc2, NULL0&ThreadID);
        CloseHandle(hThread);
        return 0;
    case WM_DESTROY:
        DeleteCriticalSection(&crit); //크리티컬 섹션 파괴.
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

주의할 점은 LeaveCriticalSection을 거쳐야지만 다른 Thread에서 다시 EnterCriticalSection을 들어갈 수 있다.

그렇기 때문에 예외 상황을 항상 고려해야하며 구조적으로 예외처리 구문제 포함시키는 것이 좋다.

 

1
2
3
4
5
6
7
__try{
    EnterCriticalSection(&crit);
    ...
}
__finally{
    LeaveCriticalSection(&crit);
};
cs

스레드 컨텍스트

 

스레드는 1개의 CPU에서 동시에 실행되는 것처럼 흉내내는 방법이므로 스레드를 어떤 순서로 얼마만큼의 간격으로 실행할 것

인가를 결정해야 하는데 이를 스케줄리이라고 한다.

운영체제는 CPU의 실행시간을 아주 잘게 쪼개어 스레드를 조금씩 순서대로 실행함으로써 동시에 실행되는 것처럼 보이게

만든다. 대략 0.02초 정도로 시간을 분할하며 이때 분할된 시간 조각 하나를 퀀텀(Quantum)이라고 한다.

또한 굉장히 빠른 속도로 스레드를 번갈아 가며 실행하는 방식을 라운드 로빈(Round Robin) 방식이라고 한다.

 

스케줄링 대상은 프로세스가 아니라 스레드이다. 즉, 워드 한번, 탐색기 한번, 특정 프로그램 한번 이렇게 스케줄링되는 것이

아니라 각 프로세스에 속한 개별 스레드들이 스케줄링되는 것이다. 또한 스케줄러는 스레드가 어떤 프로세스 소속인지에 대해서

아예 관심을 두지 않는다.

 

Thread A에서 Thread B로 작업이 스위칭된다고 생각해보자.

CPU는 다음에 다시 Thread A를 실행시킬 때 스위칭 시 중지되었던 구간이 어디인지 저장해 두어야 한다.

그렇지 않으면 다음 Thread A 실행 시 어디까지 작업을 하다 말았는지 알 수 없을 것이다.

이 때, 스레드 실행 상태에 대한 정보를 스레드 컨텍스트(Thread Context)라고 하는데 스레드가 하던 작업의 상태에 관한 정보는

단순한 레지스터값과 몇 가지 값의 집합이다. (일종의 구조체)

 

스레드 간에 작업을 전환하는 작업을 컨텍스트 스위칭이라고 하는데 매번 정보를 저장하고 읽어오는 과정이 필요하다.

그래서 실행시간 저하가 굉장히 심할 것 같지만 무시해도 될 정도로 비중이 작다. CPU는 생각보다 훨씬 더 빠르기 때문이다.

스레드 컨텍스트의 정보를 얻어와서 변경하는 함수가 제공된다. 그러나 컨텍스트 정보는 단순한 레지스터 값의 집할일 뿐이고

실행 번지를 알 수 있다 하더라도 기계어 수준에서 코드를 해석해야 하므로 사용자가 알기에는 굉장히 어렵다.

그래서 이 함수는 별도로 정리하지 않았으며 이 함수로 스레드를 임의 조작하는 것은 바람직하지 않다.

(책에는 함수에 대한 설명이 간략히 되어있음.)

 

우선순위

 

한 시스템에서 실행되는 스레드의 개수는 적어도 몇십 개이며 프로그램 몇 개만 실행되어도 백 개가 넘는 스레드가 동작한다.

이 많은 스레드를 순서대로 공평하게 실행한다면 시스템은 너무 느려질 것이다.

위에서 설명했던 것처럼 1 퀀텀이 0.02초라 치면 50개의 스레드가 실행되어도 1 스레드들을 1초마다 한 번씩 제어권을 가지게

될 것이다.

 

스레드 중에는 바쁘게 처리되는 스레드도 있고 아무것도 하지 않고 대기만 하는 스레들도 있기 때문에 시스템을 효율적으로

운영하기 위해서는 스레드간의 우선 순위를 정해 긴급하고 사용자에 더 가까운 스레드에게 좀 더 많은 시간을 주는 것이 합리적이다.

 

스레드를 생성할 때 디폴트는 보통 우선 순위 레벨은 THERAD_PRIORITY_NORMAL이며 CreateThread에는 스레드의 우선 순위 레벨을

지정하는 인수가 따로 없으므로 일단 스레드를 생성한 후 변경해야 한다.

우선 순위는 우선 순위 클래스와 우선 순위 레벨을 조합하여 정하는데 총 47가지가 되지만 거의 7~11 사이의 기본 우선 순위에서

동작한다. 결정된 순위는 스케줄러가 참조하는데 스케줄러는 무조건 순위가 높은 스레드를 우선 실행한다.

그래서 발생할 수 있는 문제점은 우선 순위가 높은 스레드 하나가 작업 중이라면 그 아래의 모든 스레드는 아무도 CPU 시간을 받을 수

없으며 이 상태를 기아(Starvation) 상태라고 한다. 그러나 실제로는 이런 상황은 발생하지 않는데 우선 순위가 높을수록 작업 시간은

짧아서 모든 스레드에게 시간이 골고루 돌아간다.

 

또한 스케줄러는 우선 순위보다도 스레드의 상태를 보고 스케줄 대상을 결정하는데 Suspend된 스레드, Sleep 중인 스레드, GetMessage

가 메시지를 꺼내지 못해 처리할 메시지가 없는 스레드 등에게는 CPU가 시간을 주지 않는다.

 

그러므로 사용자가 스레드 우선 순위를 설정할 일은 거의 없어 보인다. (내 생각)

 

 

UI 스레드

 

스레드는 보통 백그라운드에서 작업을 하며 사용자 눈에는 보이지 않는다. 이처럼 내부적인 계산만 하고 조용히 사라지는

스레드를 작업 스레드(Word Thread)라고 하는데 대부분의 스레드는 작업 스레드이다.

 

UI 스레드는 윈도우를 만들고 메시지 큐와 메시지 루프를 가진다. 메시지 큐는 스레드별로 생성되는데 UI 스레드는

주 스레드와는 다른 메시지 큐를 가지는 스레드이다. 윈도우를 가지고 메시지를 처리할 수 있다는 말은 곧 사용자와

상호 작용을 할 수 있다는 뜻이다.

 

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <Windows.h>
 
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;
LPCTSTR lpszClass="ThreadStopResume";
DWORD WINAPI ThreadFunc(LPVOID temp);
LRESULT CALLBACK DeCompProc(HWND hWnd, UINT iMessage,WPARAM wParam, LPARAM lParam);
 
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam,int nCmdShow)
{
    HWND hWnd;
    MSG Message;
    WNDCLASS WndClass;
    g_hInst=hInstance;
 
    WndClass.cbClsExtra=0;
    WndClass.cbWndExtra=0;
    WndClass.hbrBackground=(HBRUSH)(COLOR_WINDOW + 1);
    WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
    WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
    WndClass.hInstance=hInstance;
    WndClass.lpfnWndProc=(WNDPROC)WndProc;
    WndClass.lpszClassName=lpszClass;
    WndClass.lpszMenuName=NULL;
    WndClass.style=CS_HREDRAW | CS_VREDRAW;
    RegisterClass(&WndClass);
 
    WndClass.cbClsExtra = 0;
    WndClass.cbWndExtra = 0;
    WndClass.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH);
    WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    WndClass.hInstance = g_hInst;
    WndClass.lpfnWndProc = DeCompProc;
    WndClass.lpszClassName = "DecompWnd";
    WndClass.lpszMenuName = NULL;
    WndClass.style = CS_HREDRAW | CS_VREDRAW;
    RegisterClass(&WndClass);
 
    hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT,
        CW_USEDEFAULT,CW_USEDEFAULT,NULL,(HMENU)NULL,hInstance,NULL);
    ShowWindow(hWnd,nCmdShow);
    hWndMain = hWnd;
 
    while(GetMessage(&Message,0,0,0)) {
        TranslateMessage(&Message);
        DispatchMessage(&Message);
    }
    return Message.wParam;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TCHAR* Mes = "왼쪽 버튼을 누르면 압축 해제 스레드를 실행합니다.";
    HANDLE hThread;
    DWORD ThreadID;
 
    switch(iMessage) 
    {
    case WM_LBUTTONDOWN:
        hThread = CreateThread(NULL0, ThreadFunc, NULL0&ThreadID);
        CloseHandle(hThread);
        return 0;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        TextOut(hdc, 1010, Mes, lstrlen(Mes));
        EndPaint(hWnd, &ps);
        return 0;    
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
 
DWORD WINAPI ThreadFunc(LPVOID temp)
{
    HWND hWnd;
    MSG Message;
 
    hWnd = CreateWindow("DecompWnd""압축 해제 중", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
        400150, hWndMain, (HMENU)NULL, g_hInst, NULL);
    ShowWindow(hWnd, SW_SHOW);
 
    while(GetMessage(&Message, NULL00)){
        TranslateMessage(&Message);
        DispatchMessage(&Message);
    }
 
    return Message.wParam;
}
 
LRESULT CALLBACK DeCompProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
    TCHAR Cap[256];
    int Value;
 
    switch(iMessage)
    {
    case WM_CREATE:
        CreateWindow("button""시작", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 50809025, hWnd,(HMENU)0, g_hInst, NULL);
        CreateWindow("button""닫기", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 250809025, hWnd,(HMENU)1, g_hInst, NULL);
        Value = 0;
        SetProp(hWnd, "VALUE", (HANDLE)Value);
        SendMessage(hWnd, WM_COMMAND, MAKEWPARAM(0, BN_CLICKED), (LPARAM)0);
        return 0;
    case WM_TIMER:
        Value = (int)GetProp(hWnd, "VALUE");
 
        //실제 압축 코드가 있다면 Timer가 아닌 압축을 진행하는 코드를 넣으면된다.
 
        Value++;
        wsprintf(Cap, "압축 해제 중 : %d", Value);
        SetWindowText(hWnd, Cap);
        SetProp(hWnd, "VALUE",(HANDLE)Value);
        if (Value == 100) {
            SetWindowText(hWnd, "압축 해제 완료");
            KillTimer(hWnd, 1);
            EnableWindow(GetDlgItem(hWnd,0), FALSE);
        }
        return 0;
    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case 0:
            GetDlgItemText(hWnd, 0, Cap, 256);
            if (lstrcmp(Cap, "시작"== 0) {
                SetDlgItemText(hWnd,0,"중지");
                SetTimer(hWnd,1,200,NULL);
            }else{
                SetDlgItemText(hWnd,0,"시작");
                KillTimer(hWnd,1);
            }
            break;
        case 1:
            DestroyWindow(hWnd);
            break;
        }
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
 
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

ThreadFunc에서는 압축 해제 경과를 표시하는 윈도우를 만들고 메시지 루프를 돌리는데 WinMain과 흡사하다.

또한, 윈도우 클래스는 스레드 별로 중복 등록할 필요가 없으므로 WinMain에서 딱 한 번만 등록한다.

 

UI 스레드(Threadfunc)는 메시지 큐를 따로 가지는데 운영체제는 윈도우를 생성하는 스레드에 대해 별도의 메시지 큐를

생성한다. DeCompProc는 전달되는 메시지를 처리하며 압축 해제 작업을 시뮬레이션한다.

 

닫기 버튼을 누르면 DestroyWindow 함수를 호출하여 WM_DESTROY Message를 전달받고 PostQuitMessage를 호출하여

스레드의 메시지 루프를 종료시켜 스레드 자체를 종료한다.

 

 

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 3 (Thread 관리)  (0) 2017.08.29
[API] Thread 2 (MultiThread)  (0) 2017.08.29
[API] Thread 1 (단일 Thread)  (0) 2017.08.24

스레드 관리.

 

실제 스레드는 일정한 백그라운드 작업을 맡아 처리하고 작업이 끝나면 종료되는 것이 일반적이다.

작업 스레드가 백그라운드 작업을 할 때 주 스레드는 작업 스레드를 만들기만 하고 종료 상태에는 별로 관심을 두지 않는 것이

보통이다. 두 스레드(주 스레드와 작업 스레드)는 서로 독립적으로 실행되기 때문이지만 주 스레드는 적어도 작업 스레드가 종료

되었는지의 여부는 주기적으로 조사해 봐야 하는데 이때는 다음 함수가 사용된다.

 

<Thread 종료 여부 확인 함수 - GetExitCodeThread 함수.>

 

1
2
3
4
5
6
7
BOOL
WINAPI
GetExitCodeThread(
    __in  HANDLE hThread,
    __out LPDWORD lpExitCode
    );
 
cs

 

 · Parameter 1 (HANDLE hThread)

 

  - 종료 여부를 확인하고자 하는 작업 스레드의 핸들을 넘긴다.

 

 · Parameter 2 (LPDWORD lpExitCode)

 

  - Parameter 1을 넘기면 이 스레드의 종료 코드를 Parameter 2로 리턴한다.

  - 스레드가 계속 실행중이라면 STILL_ACTIVE 가 리턴된다.

  - 스레드가 종료되었다면 스레드 시작함수(앞의 예제의 ThreadFunc 함수)가 리턴한 값이나 하기에 나오는 Thread 죵료 함수인

    ExitThread 함수를 통해 전달한 종료 코드가 리턴된다.

 

 만약 앞서 만든 예제처럼 작업 스레드가 무한 루프로 돌고 있다고 해도 모든 스레드는 프로세스가 종료되면 강제로 종료되므로

 무한 루프를 도는 스레드를 만들어도 상관없다.

 

때로는 사용자가 직접 작업 중간에 스레드를 종료해야 될 경우도 있다. 스레드를 강제 종료할 때는 하기 두 함수를 사용한다.

 

<Thread 강제 종료 함수>

 

 1) ExitThread 함수.

1
2
3
4
5
VOID
WINAPI
ExitThread(
    __in DWORD dwExitCode
    );
cs

 

 ExitThread는 스레드가 스스로를 종료할 때 사용하는데 인수로 종료 코드를 전달한다.

 종료 코도는 주 스레드에서 GetExitCodeThread 함수로 받을 수 있다. 스레드가 ExitThread를 호출하면 자신의 스택을 해제하고

 연결된 DLL을 모두 분리한 후 스스로 파괴된다.

 

 2) TerminateThread 함수.

1
2
3
4
5
6
BOOL
WINAPI
TerminateThread(
    __in HANDLE hThread,
    __in DWORD dwExitCode
    );
cs

 

 TerminateThread는 스레드 핸들을 인수로 전달받아 해당 스레드를 강제로 종료한다. 다른 스레드에서 특정 스레드를 강제 종료하고자

 할 때 사용된다. 이 함수는 스레드와 연결된 DLL에게 어떠한 통보도 하지 않으므로 DLL들이 제대로 종료 처리를 하지 못할 수도 있으며,

 할당된 자원들이 제대로 해제되지 않을 수도 있다.

 

 그러므로 스레드를 종료시킬 방법이 없을 경우에만 TerminateThread 함수를 사용해야 하며, 전역 변수나 기타 다른 방법을 통해 스레드에게

 종료 명령을 전달해 스레드가 ExtiThread를 통해 스스로 종료하도록 하는 것이 가장 좋다.

 그러나 ExitThread도 c++ 객체의 소멸자가 호출되지 않고 C 런타임이 만든 고유의 데이터 블록이 해제되지 않는 문제가 발생하기도 한다.

 결국, 스레드가 작업을 무사히 마치고 return 문으로 스레드 시작 함수를 종료하는 것이 가장 바람직하다.

 

<Thread 일시 정지 관련 함수>

 

 스레드의 동작을 잠시 중지시키고 다시 재개시킬 수도 있는데 이때는 하기 함수를 사용한다.

 

 1) Thread 중지 함수.

1
2
3
4
5
DWORD
WINAPI
SuspendThread(
    __in HANDLE hThread
    );
cs

 

 2) Thread 재개 함수.

1
2
3
4
5
DWORD
WINAPI
ResumeThread(
    __in HANDLE hThread
    );
cs

 

 SuspendThread는 스레드의 동작을 중지시키고 ResumeThread는 중지된 스레드를 다시 재개시킨다.

 스레드는 내부적으로 중지 카운트를 유지하는데 이 카운트는 SuspendThread 함수가 호출되면 증가하고 ResumeThread 함수가 호출되면 감소하며

 카운트가 0이 되면 스레드는 재개된다. 그러므로 SuspendThread 함수를 호출한 횟수만큼 ResumeThread 함수를 호출해야지만 스레드가 동작한다.

 

<Thread 중지 / 재개 프로그램>

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
struct ThreadParam{
    int x,y,w,h;
    TCHAR* Mes[3];
    int interval;
} Param[3= {
    {101018050"지금 세 개의""배너가 동시에""실행되고 있습니다."100},
    {2101018050"각각 위치와""글자와 주기가""다릅니다."500},
    {4101018050"국민교육현장""국기이 대한 맹세""복무신조."1000},
};
 
 
// 버튼들의 ID
#define ID_R1 101
#define ID_R2 102
#define ID_R3 103
#define ID_PAUSE 104
#define ID_RUN 105
 
DWORD WINAPI ThreadFunc(LPVOID Param)
{
    HDC hdc;
    ThreadParam* p = (ThreadParam*)Param;
    int idx = 0;
    hdc = GetDC(hWndMain);
    for(;;)
    {
        Rectangle(hdc, p->x, p->y, p->+ p->w, p->+ p->h);
        TextOut(hdc, p->x+5, p->y+5, p->Mes[idx%3], strlen(p->Mes[idx%3]));
        GdiFlush();
        Sleep(p->interval);
        idx++;
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    static HANDLE hThread[3];
    static DWORD ThreadID[3];
    static int NowThread = 0;
    int i;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        CreateWindow("button""중지", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE, 2001008025, hWnd, (HMENU)ID_PAUSE, g_hInst, NULL);
        CreateWindow("button""실행", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE, 2001308025, hWnd, (HMENU)ID_RUN, g_hInst, NULL);
        CreateWindow("button""Thread0", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP, 10010010030, hWnd, (HMENU)ID_R1, g_hInst, NULL);
        CreateWindow("button""Thread1", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 10012010030, hWnd, (HMENU)ID_R2, g_hInst, NULL);
        CreateWindow("button""Thread2", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 10014010030, hWnd, (HMENU)ID_R3, g_hInst, NULL);
        CheckRadioButton(hWnd, ID_R1, ID_R3, ID_R1);
 
        for (i=0; i<3++i) {
            hThread[i] = CreateThread(NULL,0,ThreadFunc, &Param[i], 0&ThreadID[i]);
        }
 
        return 0;
    case WM_COMMAND:
        switch(LOWORD(wParam)) {
        case ID_R1:
            NowThread = 0;
            break;
        case ID_R2:
            NowThread = 1;
            break;
        case ID_R3:
            NowThread = 2;
            break;
        case ID_PAUSE:
            SuspendThread(hThread[NowThread]);
            break;
        case ID_RUN:
            ResumeThread(hThread[NowThread]);
            break;
        }
 
        return 0;
    case WM_DESTROY:
        for (i=0; i<3++i)
            CloseHandle(hThread[i]);
 
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
 
 
cs

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 2 (MultiThread)  (0) 2017.08.29
[API] Thread 1 (단일 Thread)  (0) 2017.08.24

Thread 1에서는 단일 스레드를 생성하였고 이번에는 멀티 스레드를 생성하여 동작을 확인해 볼 것이다.

 

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
DWORD WINAPI ThreadFunc(LPVOID prc)
{
    HDC hdc;
    BYTE Blue = 0;
    HBRUSH hBrush, hOldBrush;
    RECT rc = *(LPRECT)prc;
    hdc = GetDC(hWndMain);
    for(;;)
    {
        Blue+=5;
        Sleep(20);
        hBrush = CreateSolidBrush(RGB(0,0,Blue));
        hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);
        Rectangle(hdc, rc.left,rc.top,rc.right,rc.bottom);
        SelectObject(hdc, hOldBrush);
        DeleteObject(hBrush);
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HDC hdc;
    DWORD ThreadID;
    static RECT arRect[] = {
        {100100200200}, {300100400200},
        {100300200400}, {300300400400},
    };
 
    int i;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        for (i=0; i<4++i) {
            CloseHandle(CreateThread(NULL,0,ThreadFunc, &arRect[i], 0&ThreadID));
        }
        return TRUE;
    case WM_LBUTTONDOWN:
        hdc = GetDC(hWnd);
        Ellipse(hdc, LOWORD(lParam)-10, HIWORD(lParam)-10, LOWORD(lParam)+10, HIWORD(lParam)+10);
        ReleaseDC(hWnd, hdc);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

상기 소스는 동시에 4개의 스레드가 실행되면서 파란색 사각형을 계속 그리고 있으며 주 스레드는 왼쪽 마우스 클릭 입력을 받아

원을 그리는 동작을 한다. 주 스레드를 포함한 5개의 스레드가 동시에 실행되고 있는 것이다.

 

WM_CREATE에서 4개의 스레드를 생성하는데 같은 동작을 하는 스레드이므로 시작 함수는 모두 ThreadFunc이다.

같은 프로세스 내의 Thread끼리는 주소 공간, 전역 변수, 코드를 공유하므로 같은 시작함수를 사용해도 상관없다.

시작 함수가 같더라도 전달되는 인수가 다르면 다른 동작을 한다. 그러므로 차후에 멀티 스레드를 사용하게 되면 같은 자원에 대해

Thread끼리 자원에 접근하는 순서를 제어할 필요가 있게 된다. (동기화 처리)

 

여기서는 앞에와 다르게 ThreadFunc의 인자를 CreateThread 4번째 인자로 넘겨주었다.

스레드로 전달되는 인자는 LPVOID 형이기 때문에 실제로 어떤 형태의 인수든지 전달할 수 있다.

간단한 정수형도 가능하고 크기가 큰 데이터라면 구조체를 만든 후 그 포인터를 전달하면 된다.

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 3 (Thread 관리)  (0) 2017.08.29
[API] Thread 1 (단일 Thread)  (0) 2017.08.24

해당 내용들은 김상형 저자님의 윈도우즈 API 정복 #2 에 나오는 내용들입니다.

(API의 바이블이라고 생각하는 도서이며 옆에 껴두고 필요한 부분들을 참고하면서 공부하기 정말 좋은 책입니다.)

 

한 프로그램에서 여러 가지 작업을 동시에 수행해야 할 경우가 있다.

대체로 cpu가 하나뿐이며 폰 노이만형 컴퓨터는 한 번에 하나의 일만 할 수 있으므로 실제로 이것은 불가능하다.

그리하여 나온 것이 Thread 개념이며 여러 Thread들이 번갈아 가면서 조금씩 작업을 함으로써 동시에 실행되는 듯한

효과를 낼 수 있다.

 

Thread는 프로세스 내에 존재하는 일련의 실행 코드이다.

프로세스는 단지 존재하기만 하는 껍데기일 뿐이며 실제 작업은 스레드가 담당한다.

프로세스 생성 시 하나의 주 스레드가 생성되며 대부분의 경우 주 스레드가 모든 작업을 처리하고 주 스레드가 종료되면

프로세스도 같이 종료된다.

 

스레드가 여러개 생긴 경우라면, 주 스레드와 나머지 스레드들은 CPU 시간을 우선 순위에 따라 적절하게 분배하여 동시에

실행된다. 운영체제는 스레드별로 골고루 CPU 시간을 배분하므로 한 스레드가 시간을 지나치게 오래 끌더라도 다른 스레드가

이에 영향을 받지 않고 실행된다.

그러므로 이 방법은 타이머나 PeekMessage를 사용하는 방법보다 반응성이 훨씬 좋다.

 

하나의 운영체제에 여러 개의 프로세스가 동시에 실행되는 환경을 멀티 태스킹이라하며 하나의 프로세스에서 여러 개의 스레드가

동시에 실행되는 환경은 멀티스레드라 한다.

 

<간단한 Thread 생성 코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    DWORD ThreadID;
    HANDLE hThread;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        hThread = CreateThread(NULL,0,ThreadFunc, NULL0&ThreadID);
        CloseHandle(hThread);
        return TRUE;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
 
cs

 

우선 상기 코드는 주 스레드의 WM_CREATE에서 스레드를 하나 생성한 코드이며 CreateThread 함수를 사용한다.

 

<CreateThread 함수>

1
2
3
4
5
6
7
8
9
10
HANDLE
WINAPI
CreateThread(
    __in_opt  LPSECURITY_ATTRIBUTES lpThreadAttributes,
    __in      SIZE_T dwStackSize,
    __in      LPTHREAD_START_ROUTINE lpStartAddress,
    __in_opt  LPVOID lpParameter,
    __in      DWORD dwCreationFlags,
    __out_opt LPDWORD lpThreadId
    );
cs

 

 · Parameter 1 (LPSECURITY_ATTRIBUTES lpThreadAttributes)

 

  - 스레드의 보안 속성을 지정하는데 자식 프로세스로 핸들을 상속하지 않는 한 NULL로 지정하면 된다.

 

 · Parameter 2 (SIZE_T dwStackSize)

 

  - 스레드의 스택 크기를 지정하는데 스레드끼리 상호 안정된 동작을 하기 위해 스레드별로 별도의 스택이 할당된다.

  - 스택의 크기를 0으로 지정하면 주 스레드와 같은 크기를 가진다.

 

 · Parameter 3 (LPTHREAD_START_ROUTINE lpStartAddress)

 

  - 스레드의 시작 함수(Entry Point)를 지정하며 실질적으로 가장 중요한 인자. (주 스레드의 WinMain 함수에 해당)

  - 지정된 함수로부터 스레드의 실행을 시작하며 해당 함수가 종료되면 스레드도 종료된다.

  - 시작 함수는 다음과 같은 원형을 가져야 한다. → DWORD WINAPI ThreadFunc(LPVOID lpParameter);

 

 · Parameter 4 (LPVOID lpParameter)

  

  - Parameter 3에서 지정한 스레드의 시작 함수는 LPVOID형의 인수 하나만 받아들이는데 이 인수는 CreateThread의

    네 번째 인수로 지정한다.

  - 4 번째 인수는 스레드로 전달할 작업 내용을 지정한 것이며, 전달할 내용이 없다면 NULL을 전달하면 된다.

 

 · Parameter 5 (DWORD dwCreationFlags)

 

  - 생성할 스레드의 특성을 지정하는 인자이며 아무 특성이 없는 보통 스레드를 만들고자 한다면 0으로 지정하면 된다.

  - 만약 스레드를 만들기만 하고 실행은 하지 않게 하고 싶다면, CREATE_SUSPENDED 플래그를 지정하면 된다.

 

 · Parameter 6 (LPDWORD lpThreadId)

 

  - CreateThread 함수가 스레드를 만든 후 스레드의 ID를 리턴하기 위한 출력용 인수이므로 DWORD형의 변수를 하나
    선언한 후 주소값을 넘겨준다.

 

CreateThread 함수는 스레드를 만든 후 스레드의 핸들을 리턴하며 에러가 발생했을 경우 NULL을 리턴한다.

리턴된 핸들을 이용하여 스레드를 제어하는데 상기 예제는 생성 후 스레드를 제어하지 않기 때문에 바로 핸들을 닫았다.

스레드 핸들과 스레드 자체는 다르므로 핸들을 닫는다고 해서 스레드가 종료되는 것은 아니다.

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 3 (Thread 관리)  (0) 2017.08.29
[API] Thread 2 (MultiThread)  (0) 2017.08.29

+ Recent posts