본문 바로가기

High Level Technique/Window System

쓰레드 동기화

쓰레드 동기화


동기화라는 단어는 무언가 상태가 동일함을 말하는데 쓰레드 동기화는 정해진 순서가 잘 지켜지고 있음을 말합니다.

A 쓰레드의 결과를 B쓰레드가 출력을 한다고 가정할때 정상적인 순서로는 A 쓰레드가 실행이 된 후 B 쓰레드가 실행이 되어야 합니다. 하지만 B 쓰레드가 먼저 실행되면 정상적으로 출력할 수 없게 되죠. 따라서 쓰레드의 실행 순서를 정의하고 이 순서에 맞게 따르도록 하는 것이 쓰레드 동기화 라고 합니다.


A 쓰레드와 B 쓰레드가 같은 메모리 영역에(데이터, 힙)에 접근한다면 문제가 발생할 것입니다. 이러한 동시 접근을 막는것 도한 동기화에 해당합니다.




동기화의 두가지 방법


유저모드 동기화

동기화가 진행되면서 커널의 힘을 빌리지 않는 기법을 말합니다. 커널 모드로 전환이 불필요하기 때문에 성능상에는 이점이 있지만 기능상에 제약이 있습니다.


커널모드 동기화

커널에서 제공하는 동기화 기능을 이용하는 기법입니다. 커널 모드로 변경이 필요하기 대문에 성능 저하로 이어지지만 유저모드 동기화에서 사용하지 못하는 기능을 사용할 수 있습니다.





임계 영역 (Critical Section) 접근 동기화


여러 쓰레드가 같은 영역에 동시에 접근하는 경우 해당 코드 영역을 임계영역이라고 합니다. 이러한 임계 영역은 한 순간에 한 쓰레드만 접근이 요구되는 코드 영역입니다.



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
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_gate 6
 
LONG gTotalCount = 0;
 
void InCreaseCount()
{
    gTotalCount++;
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 1000; i++)
    {
        InCreaseCount();
    }
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[num_of_gate];
    HANDLE hThread[num_of_gate];
 
    for (int i = 0; i < num_of_gate; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadID[i]);
 
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_gate, hThread, TRUE, INFINITE);
 
    printf("Total Count: %d \n", gTotalCount);
 
    for (int i = 0; i < num_of_gate; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    return 0;
}
cs


쓰레드가 num_of_gate의 수만큼 생성이 되고 각 쓰레드는 1000씩 InCreaseCount()를 호출합니다.


실행결과를 보면 정상적으로 6000이 나오게 됩니다.


하지만 InCreaseCount()를 호출하는 수를 100000으로 늘리는 경우 아래와 같은 결과를 출력합니다.



600000이 되어야 정상일텐데 위 결과를 보면 전혀다른 값을 출력하고 있습니다. 여러 쓰레드가 gTotalCount++;에 접근을 하기 때문에 발생하는 결과입니다.


이렇게 여러 쓰레드가 접근하여 위와 같은 결과를 발생할 수 있는 영역을 임계영역이라고 합니다.


이러한 결과를 방지하기 위해서 임계 영역에 접근하는 쓰레드를 한 순간에 하나로 지정을 해야합니다.







유저 모드 동기화


Critical Section 동기화


앞서 임계영역 동기화에 대해서 설명했는데 임계영역 또한 Critical Section으로 용어가 같습니다. 하지만 다른 의미로 사용됩니다. 둘다 Critical Section이라고 생각해도 좋지만 하나는 임계영역, 하나는 크리티컬 섹션 이렇게 이해하는 것이 좋을것 같습니다.



책을 보면 정말 아주 쉽게 설명을 해놓았다. (정말 적절한 비유.) 화장싱을 가기 위해서 열쇠가 필요하다면 열쇠를 가져가 화장실을 간 뒤 다시 열쇠를 제자리에 두어야 다음 사람이 사용할 수 있게 된다.

앞서 임계영역 소스코드를 예로 들면 gTotalCount++;에 접근을 하려면(화장실) 접근권한(열쇠)가 필요한 것이다.



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
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_gate 6
 
LONG gTotalCount = 0;
 
CRITICAL_SECTION hCriticalSection; //화장실 열쇠
 
void InCreaseCount()
{
    EnterCriticalSection(&hCriticalSection); // 열쇠를 이용해 화장실에 들어감.
    gTotalCount++;
    LeaveCriticalSection(&hCriticalSection); // 화장실에서 나옴 열쇠를 제자리에 둠.
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 1000000; i++)
    {
        InCreaseCount();
    }
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[num_of_gate];
    HANDLE hThread[num_of_gate];
 
    InitializeCriticalSection(&hCriticalSection); // 열쇠를 걸어둠.
 
    for (int i = 0; i < num_of_gate; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadID[i]);
 
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_gate, hThread, TRUE, INFINITE);
 
    printf("Total Count: %d \n", gTotalCount);
 
    for (int i = 0; i < num_of_gate; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    DeleteCriticalSection(&hCriticalSection);
 
    return 0;
}
cs



앞서 InCreaseCount()에 접근하는 수를 100000으로 했을 때 전혀 다른 값이 나온다고 말했는데 CriticalSection 동기화를 이용해서 정상적으로 출력되는지 확인해 본다.



정상적으로 출력되는 것을 볼 수 있다. 실행에는 아주 작은 시간이 걸리긴 한다.




인터락 동기화


전역으로 선언된 변수 하나만을 동기화 하는 것이 목적이라면 인터락 동기화를 사용해도 됩니다. 인터락 동기화는 내부적으로 한 순간에 한 쓰레드만 실행 되도록 동기화 되어 있습니다.

인터락 함수는 2가지가 존재합니다. InterlockedIncrement(), InterlockedDecrement()


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
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_gate 6
 
LONG gTotalCount = 0;
 
 
void InCreaseCount()
{
    
    InterlockedIncrement(&gTotalCount);
    
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 1000000; i++)
    {
        InCreaseCount();
    }
 
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[num_of_gate];
    HANDLE hThread[num_of_gate];
 
    
 
    for (int i = 0; i < num_of_gate; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadID[i]);
 
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_gate, hThread, TRUE, INFINITE);
 
    printf("Total Count: %d \n", gTotalCount);
 
    for (int i = 0; i < num_of_gate; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    return 0;
}
cs


단 한줄로 깔끔하게 쓰레드의 동시 접근을 막을 수 있습니다.



volatile 키워드는 최적화 수행을 하지 않게하거나 메모리 영역을 캐쉬하지 않게 하기 위해서 사용된다.









커널 모드 동기화


뮤텍스 동기화


크리티컬 섹션과 마찬가지로 CreateMutex()를 통해서 열쇠를 만든다. 해당 함수의 반환형을 보면 HANDLE로 되어있는데 뮤텍스가 커널 오브젝트라는 것을 알 수 있다.

크리티컬 섹션 동기화와는 다르게 함수 호출 과정에서 초기화가 일어난다.


커널 오브젝트에 대해 설명했을 때 커널 오브젝트는 Signaled 상태와 Non-Signaled 상태가 존재한다고 했다. 어느 한 쓰레드가 열쇠를 가지고 있으면 Non-Signaled 상태가 되고 열쇠를 반환 했을 때 Signaled 상태가 된다.


뮤텍스는 커널 오브젝트이고 획득이 가능할 때 Signaled 상태에 놓인다. WaitForSingleObject()를 임계 영역 진입을 위한 뮤텍스 획득 용도로 사용된다. 반면에 뮤텍스를 반환할 때 ReleaseMutex()을 사용하면서 뮤텍스는 다시 Signaled 상태로 된다.


WaitForSingleOjbect()의 특성상 커널 오브젝트가 Signaled 상태가 되어 반환하는 경우 해당 커널 오브젝트가 Non-Signaled 상태로 변경된다. 이런 과정이 이루어지면 다른 쓰레드는 임계 영역 진입에 제한이 된다.


임계영역에서 일을 끝낸 쓰레드가 임계 영역을 빠져나오면서 ReleaseMutex 함수를 호출한다. 이 함수가 호출되면서 뮤텍스는 다른 누군가에 획득상태 Signaled 상태가 되어 쓰레드의 진입을 허용한다.


DeleteCriticalSection()에 해당하는 함수는 없고 CloseHandle()로 반환하면 된다.


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
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_gate 6
 
LONG gTotalCount = 0;
HANDLE hMutex;
 
void InCreaseCount()
{
 
    WaitForSingleObject(hMutex, INFINITE);
    gTotalCount++;
    ReleaseMutex(hMutex);
 
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 1000000; i++)
    {
        InCreaseCount();
    }
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[num_of_gate];
    HANDLE hThread[num_of_gate];
 
    hMutex = CreateMutex(NULL, FALSE, NULL);
 
    if (hMutex == NULL)
    {
        printf("CreateMutex Error: %d \n", GetLastError());
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadID[i]);
 
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_gate, hThread, TRUE, INFINITE);
 
    printf("Total Count: %d \n", gTotalCount);
 
    for (int i = 0; i < num_of_gate; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    CloseHandle(hMutex);
 
    return 0;
}
cs





세마포어 동기화


어느 한 식당에 손님을 10명 정도 받을 수 있고 예상 인원이 50명, 각 손님은 10~30분 정도의 시간을 소비한다고 하자.

테이블이 10개이기 때문에 임계영역에 접근할 수 있는 쓰레드는 10개이다.


뮤텍스와 다르게 세마포어에는 카운트 기능이 있어 쓰레드의 개수를 조정할 수 있다.


세마포어의 인자 중 lInitialCount에 의해 초기카운트가 결정되며 0인 경우 Non-Signaled, 1인 경우 Signaled 상태가 된다.


WaitForSingleObject()에 의해 그 값이 하나씩 줄어들고 초기 카운트로 10으로 했을 때 총 10번의 WaitForSingleObject()가 호출되어 값을 하나씩 감소시킨다.


11번째 호출이 있을 경우 세마포어 카운트가 0인 관계로 Blocking 상태가 된다.


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
#include <stdio.h>
#include <tchar.h>
#include <time.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_customer 50
#define range_min 10
#define range_max (30-range_min)
#define table_cnt 10
 
HANDLE hSemaphore;
DWORD randTimeArr[50];
 
void TakeMeal(DWORD time)
{
    WaitForSingleObject(hSemaphore, INFINITE);
    printf("Enter Customer %d \n", GetCurrentThreadId());
 
    printf("Customer %d having launch \n", GetCurrentThreadId());
 
    Sleep(1000 * time);
 
    ReleaseSemaphore(hSemaphore, 1NULL);
    printf("Out Customer %d \n\n", GetCurrentThreadId());
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    TakeMeal((DWORD)lpParam);
    return 0;
}
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadID[num_of_customer];
    HANDLE hThread[num_of_customer];
 
    srand((unsigned)time(NULL));
 
    for (int i = 0; i < num_of_customer; i++)
    {
        randTimeArr[i] = (DWORD) ( ((double)rand() / (double)RAND_MAX ) * range_max + range_min);
    }
 
    hSemaphore = CreateSemaphore(NULL, table_cnt, table_cnt, NULL);
 
    if (hSemaphore == NULL)
    {
        printf("CreateSemaphore Error: %d \n", GetLastError());
    }
 
    for (int i = 0; i < num_of_customer; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL0, ThreadProc, (void*)randTimeArr[i], CREATE_SUSPENDED, (unsigned*)&dwThreadID[i]);
 
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_customer; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_customer, hThread, TRUE, INFINITE);
 
    printf("==END==\n");
 
    for (int i = 0; i < num_of_customer; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    CloseHandle(hSemaphore);
 
    return 0;
}
cs



출력결과는 아래와 같다.


Enter Customer 6684

Customer 6684 having launch

Enter Customer 13240

Customer 13240 having launch

Enter Customer 11580

Customer 11580 having launch

Enter Customer 8396

Customer 8396 having launch

Enter Customer 8284

Customer 8284 having launch

Enter Customer 8576

Customer 8576 having launch

Enter Customer 9828

Customer 9828 having launch

Enter Customer 7728

Customer 7728 having launch

Enter Customer 14408

Customer 14408 having launch

Enter Customer 8928

Customer 8928 having launch

Out Customer 6684


Enter Customer 7668

Customer 7668 having launch

Out Customer 9828


Enter Customer 15168

Customer 15168 having launch

Out Customer 8928


Enter Customer 14804

Customer 14804 having launch

Out Customer 8576


Enter Customer 6032

Customer 6032 having launch

Out Customer 13240


Enter Customer 15188

Customer 15188 having launch

Enter Customer 15112

Customer 15112 having launch

Out Customer 7728


Out Customer 8284


Enter Customer 720

Customer 720 having launch

Out Customer 14804


Enter Customer 13236

Customer 13236 having launch

Out Customer 14408


Enter Customer 5292

Customer 5292 having launch

Enter Customer 6560

Customer 6560 having launch

Out Customer 8396


Out Customer 11580


Enter Customer 11968

Customer 11968 having launch

Out Customer 15112


Enter Customer 3244

Customer 3244 having launch

Out Customer 6032


Enter Customer 7148

Customer 7148 having launch

Out Customer 5292


Enter Customer 9484

Customer 9484 having launch

Out Customer 15188


Enter Customer 1672

Customer 1672 having launch

Out Customer 7668


Enter Customer 12556

Customer 12556 having launch

Out Customer 11968


Enter Customer 7720

Customer 7720 having launch

Out Customer 15168


Enter Customer 5088

Customer 5088 having launch

Out Customer 6560


Enter Customer 4432

Customer 4432 having launch

Out Customer 3244


Enter Customer 5144

Customer 5144 having launch

Out Customer 720


Enter Customer 3964

Customer 3964 having launch

Out Customer 13236


Enter Customer 3212

Customer 3212 having launch

Out Customer 1672


Enter Customer 6492

Customer 6492 having launch

Out Customer 9484


Enter Customer 12804

Customer 12804 having launch

Out Customer 12556


Enter Customer 9128

Customer 9128 having launch

Out Customer 7720


Enter Customer 9308

Customer 9308 having launch

Out Customer 7148


Enter Customer 2040

Customer 2040 having launch

Out Customer 4432


Enter Customer 2392

Customer 2392 having launch

Out Customer 5088


Enter Customer 8716

Customer 8716 having launch

Enter Customer 2800

Customer 2800 having launch

Out Customer 5144


Out Customer 6492


Enter Customer 13992

Customer 13992 having launch

Out Customer 12804


Enter Customer 6952

Customer 6952 having launch

Out Customer 3212


Enter Customer 13036

Customer 13036 having launch

Out Customer 9308


Enter Customer 980

Customer 980 having launch

Out Customer 3964


Enter Customer 3792

Customer 3792 having launch

Out Customer 9128


Enter Customer 7100

Customer 7100 having launch

Enter Customer 12168

Customer 12168 having launch

Out Customer 2040


Out Customer 13992


Enter Customer 10316

Customer 10316 having launch

Enter Customer 5140

Customer 5140 having launch

Out Customer 8716


Out Customer 2392


Enter Customer 11040

Customer 11040 having launch

Out Customer 10316


Out Customer 5140


Out Customer 3792


Out Customer 2800


Out Customer 7100


Out Customer 6952


Out Customer 12168


Out Customer 13036


Out Customer 11040


Out Customer 980


==END==

계속하려면 아무 키나 누르십시오 . . .



Named Mutex


뮤텍스에 대해서 설명할 때 커널 오브젝트라고 했습니다. 커널 오브젝트는 서로 다른 프로세스에서 접근을 할 수 있죠. 핸들 테이블에 같은 정보다 들어있다면!


각각의 핸들 테이블은 프로세스마다 다르기 때문에 A 프로세스의 핸들 테이블에 해당 커널 오브젝트의 핸들이 있다고 해서 B에서 무조건 접근할 수 있는 것이 아니라고 했습니다.


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
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
HANDLE hMutex;
DWORD dwWaitResult;
 
void ProcessBaseCriticalSection()
{
    dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
 
    switch (dwWaitResult)
    {
    case WAIT_OBJECT_0:
        printf("Thread got mutex! \n");
        break;
        
    case WAIT_TIMEOUT:
        printf("timer expired! \n");
        break;
 
    case WAIT_ABANDONED:
        return;
    }
 
    for (int i = 0; i < 5; i++)
    {
        printf("Thread Running! \n");
        Sleep(10000);
    }
 
    ReleaseMutex(hMutex);
}
 
int _tmain(int argc, TCHAR* argv[])
{
#if 0
    hMutex = CreateMutex(NULL, FALSE, _T("NamedMutex"));
#else
    hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, _T("NamedMutex"));
#endif
 
    if (hMutex == NULL)
    {
        printf("CreateMutex Error: %d \n", GetLastError());
        return -1;
    }
 
    ProcessBaseCriticalSection();
 
    CloseHandle(hMutex);
 
    return 0;
}
cs


위 코드를 컴파일 해서 CreateMutex.exe로 만들고 다른 하나는 라인 38을 if 1로 변경하여 OpenMutex.exe로 만든다.


먼저 CreateMutex.exe를 실행시키고 OpenMutex.exe를 실행시킨다. CreateMutex.exe가 실행이 다 되고 뮤텍스가 반환이 되면 OpenMutex.exe가 실행이 된다.



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

쓰레드 풀 (Thread Pool)  (0) 2017.01.04
생산자/소비자 모델  (1) 2017.01.04
Thread 생성과 소멸  (0) 2017.01.02
쓰레드 (Thread)  (0) 2017.01.02
우선순위 알고리즘  (0) 2017.01.02