크리티컬 섹션(Critical Section)
크리티컬 섹션은 개념적 크리티컬 섹션과 기능적 크리티컬 섹션 두가지 의미로 쓰일 수 있다.
우선 개념적 크리티컬 섹션은 다음과 같다.
여러개의 스레드가 돌아가고 있는 상황에서 하나의 공유 리소스(ex : 전역 변수)를 조작하는데
a,b 스레드가 둘다 +1을 하는 과정을 100번 할 때 정답은 200이 나와야하는데 200이 나오지 않게 된다.
그 이유는 크리티컬 섹션에 접근 할 때 스케줄러에 의해 컨텍스트 스위칭을 계속해서 당하기 때문이다.
아래 코드를 실행하면 그 이유를 알 수 있다.
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 | // Using Mutex #include <pthread.h> #include <stdio.h> pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; int sum = 0; void *threadRoutine(void *argumentPointer) { int i; // 이 부분이 Critical Section에 해당한다. for(i = 0; i < 1000000; i ++) sum++; return NULL; } int main() { pthread_t threadID1, threadID2; pthread_create(&threadID1, NULL, threadRoutine, NULL); pthread_create(&threadID2, NULL, threadRoutine, NULL); pthread_join(threadID1, NULL); pthread_join(threadID2, NULL); printf("결과 합 :: %d\n",sum1); return 0; } // This source code Copyright belongs to Crocus // If you want to see more? click here >> | Crocus |
따라서 이러한 공유 변수를 함께 이용하는 구간을 임계 구역, 크리티컬 섹션(Critical Section)이라 부른다.
이제 기능적 크리티컬 섹션에 대해 이야기 해보고자 한다.
기능적 크리티컬 섹션이라 하는 이유는 위와 같은 상황을 막기 위해(동기화 시키기 위해) 크리티컬 섹션을 관리하기 위한 크리티컬 섹션 객체에 대해 설명하기 위함이다.
크리티컬 섹션은 프로세스 하나에 포함된 여러 개의 스레드가 공유 리소스에 접근할 때 배타적 제어를 하기 위한 구조다.
크리티컬 섹션 자체는 Windows 객체 중 하나며, 프로그램에서 CRITICAL_SECTION 타입 변수를 선언해서 사용한다.
일반 Windows 객체와 달리 프로세스의 메모리 공간에 확보된 변수를 이용하기 때문에 동일한 프로세스 내의 스레드 동기화에 사용할 수는 있지만, 다른 프로세스 간 동기화에는 사용할 수 없다.
배타적 제어를 하려면 공유 리소스에 대해 크리티컬 섹션 객체를 정의한 뒤 접근하고 싶은 스레드는 우선 그 객체의 소유권을 시스템에 요구한다.
소유권을 얻은 스레드는 리소스에 접근하고, 그 후에 소유권을 반납한다. 소유권은 하나뿐이므로 리소스에 접근할 수 있는 스레드 역시 늘 하나뿐이다.
TV 퀴즈 프로그램에서 먼저 부저를 누른 사람이 답을 말할 권리를 얻는 것과 같은 상황이다.
크리티컬 섹션에서 배타적 제어를 하기 위해서는 미리 InitializeCriticalSection로 변수를 초기화해 두어야 한다. 초기화한 객체에 대해서 EnterCriticalSection나 TryEnterCriticalSection로 소유권 획득을 요구하며, 리소스를 모두 사용했으면 LeaveCriticalSection로 소유권을 반납한다.
크리티컬 섹션을 사용할 경우에는 공유 리소스에 접근하는 부분의 코드를 정확히 EnterCriticalSection와 LeaveCriticalSection로 둘러싸는 형태가 된다. 둘러싸인 코드 부분은 접근 시 스레드 간 경합을 일으킬 가능성이 있는 위험한 구역으로, 말 그대로 크리티컬 섹션인 것이다.
다른 스레드에 소유권이 있어서 획득할 수 없는 경우 EnterCriticalSection을 호출한 스레드는 대기 상태로 들어가서 돌아오지 않는다. 그리고 소유권을 획득할 수 있는 상태가 되면 Windows가 자동으로 스레드 실행을 재개해 준다.
대기 상태중에는 CPU 파워를 소비하지 않기 때문에 busy wait 문제가 발생하지 않는다. EnterCriticalSection대신 TryEnterCriticalSection를 사용하면 소유권 획득 여부와는 상관없이 곧바로 복귀한다. 즉 이를 이용하면 소유권을 획득할 수 있을 때까지 다른 처리를 계속 진행할 수 있다.
결국 크리티컬 섹션을 정리하면 다음과 같다.
1. 동일한 프로세스 내의 여러 스레드 사이에서만 동기화가 가능하다.
2. 커널 모드 객체(Mutex, Semaphore)가 아닌 유저 모드 동기화 객체이므로 가볍고 빠르다.
3. 먼저 접근한 스레드는 EnterCiricalSection을 통해 락을 획득하고, 그 이후에 Critical Section에 들어오고자 하는 스레드는 대기시킨다. 이후 LeaveCriticalSection으로 락을 해제 하게되면 대기중이던 다른 스레드가 접근 할 수 있게 된다.
4. 대기 중인 스레드는 다른 스레드에게 CPU를 yield해야하므로 컨텍스트 스위칭이 발생하고 대기 시간동안에는 CPU 점유를 하지 않게 된다.
이때 4번에서 컨텍스트 스위칭을 발생시키지 않기 위해서는 스핀 락(Spin lock) 방식을 이용하기도 한다.
스핀락은 아래 사이트에서 매우 자세히 설명해주고 있으니 참고해보자.
http://jake.dothome.co.kr/spinlock/
예제 코드
아래 실제로 Critical Section에 관한 예제 코드를 보자.
아래 코드는 유저 모드 객체를 이용한 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 | #include <iostream> #include <thread> #include <windows.h> using namespace std; CRITICAL_SECTION cs; int cnt1, cnt2; void func1() { for (int i = 1; i <= 1000000; i++) cnt1++; } void func2() { EnterCriticalSection(&cs); for (int i = 1; i <= 1000000; i++) cnt2++; LeaveCriticalSection(&cs); } int main() { InitializeCriticalSection(&cs); for (int i = 0; i < 10; i++) { cnt1 = cnt2 = 0; thread t1(func1); thread t2(func1); thread t3(func2); thread t4(func2); t1.join(); t2.join(); t3.join(); t4.join(); printf("Critical Section 사용 X :: %d\n", cnt1); printf("Critical Section 사용 O :: %d\n", cnt2); } return 0; } // This source code Copyright belongs to Crocus // If you want to see more? click here >> | Crocus |
아래 코드는 커널 모드 객체인 뮤텍스를 이용하여 크리티컬 섹션을 관리하고 있다.
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 | // Using Mutex #include <pthread.h> #include <stdio.h> pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; int sum = 0; int sum1 = 0; void *threadRoutine(void *argumentPointer) { int i; // 뮤텍스 락을 거는 과정 pthread_mutex_lock(&counter_mutex); // 이 부분이 Critical Section에 해당한다. for(i = 0; i < 1000000; i ++) sum++; // 뮤텍스 락을 푸는 과정 pthread_mutex_unlock(&counter_mutex); return NULL; } void *threadRoutine1(void *argumentPointer) { int i; // 이 부분이 Critical Section에 해당한다. for(i = 0; i < 1000000; i ++) sum1++; return NULL; } int main() { pthread_t threadID1, threadID2; pthread_t threadID3, threadID4; // Create < 뮤텍스 이용 > pthread_create(&threadID1, NULL, threadRoutine, NULL); pthread_create(&threadID2, NULL, threadRoutine, NULL); // Create < 뮤텍스 이용 x > pthread_create(&threadID3, NULL, threadRoutine1, NULL); pthread_create(&threadID4, NULL, threadRoutine1, NULL); // Join < 뮤텍스 이용 > pthread_join(threadID1, NULL); pthread_join(threadID2, NULL); // Join < 뮤텍스 이용 x > pthread_join(threadID3, NULL); pthread_join(threadID4, NULL); printf("뮤텍스를 이용한 결과 합 :: %d\n",sum); printf("뮤텍스를 이용하지 않은 결과 합 :: %d\n",sum1); return 0; } // This source code Copyright belongs to Crocus // If you want to see more? click here >> | Crocus |
threadRoutine은 EnterCriticalSection을 이용하여 하나의 스레드가 크리티컬 섹션에서 일을 수행하는 동안 다른 하나의 스레드는 대기 상태로 들어가 있다가 LeaveCriticalSection이 Call 됐을 때 나머지 스레드가 돌아가게되어 2000000의 값을 꾸준히 얻게 되고
threadRoutine1은 크리티컬 섹션으로 프로그래머가 생각을 하고 있어도 임계 구역의 동기화가 설정되어 있지 않아 값이 랜덤하게 나옴을 알 수 있다.
'Applied > Operating System(OS)' 카테고리의 다른 글
프로세서, 메모리, 캐시 개념 및 원리 (0) | 2018.10.02 |
---|---|
컨택스트 스위칭(Context Switching) (0) | 2018.10.01 |
CPU,GPU, GPGPU (0) | 2018.09.30 |
뮤텍스(Mutex), 세마포어(Semaphore), 모니터(Monitor) (0) | 2018.05.05 |
커널 레벨 스레드 vs 사용자 레벨 스레드 (0) | 2018.05.01 |