멀티 스레드의 문제점

 

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

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

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

의해 지워진다.

이와 같이 스레드가 공유 자원을 서로 사용하려는 상태를 경쟁 상태(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

+ Recent posts