쓰레드 동기화
동기화라는 단어는 무언가 상태가 동일함을 말하는데 쓰레드 동기화는 정해진 순서가 잘 지켜지고 있음을 말합니다.
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(NULL, 0, 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(NULL, 0, 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(NULL, 0, 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(NULL, 0, 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, 1, NULL); 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(NULL, 0, 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 |