메뉴 건너뛰기


Developer > Application


3장. 크리티컬 섹션(CriticalSection)과 인터락(InterLock) 함수
 
이번에는 동기화 중에서 Windows에서만 지원되는 기능인 크리티컬 섹션과 인터락 함수에 대해서 알아보도록 하겠습니다.
 
1. 크리티컬 섹션(Critical Section)
 
크리티컬 섹션을 해석하면 "임계영역"이라고 합니다. 책에서 그렇다는 것이고 저보고 해석하라고 하면 "위험영역"이라고 하고 싶군요.
즉, Source코드 중에 쓰레드에 의해 서로 간섭이 일어나 문제가 일어날 소지가 있는 코드부분을 위험 영역이라고 정하고 이들 구간은 하나의 쓰레드만이 실행하도록 하는 것입니다.
크리티컬 섹션은 보호되어야 할 영역을 나타내는 일반적인 용어이지만 여기서는 윈도우즈에서 제공하는 특정 함수를 나타내는 용어로 사용됩니다. 
크리티컬 섹션은 나중에 이야기할 뮤텍스나 세마포어보다 사용자 모드에서 동작하므로 성능이 좋지만 하나의 프로세스안에서 여러 쓰레드간의 동기화만을 처리한다는 제한이 있습니다. 참고로 뮤텍스나 세마포어는 커널 모드에서 동작되는 함수입니다.
 
크리티컬 섹션에 사용하는 함수는 총 4가지가 있습니다.
 
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCrtiticalSection);
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
 
크리티컬 섹션에 사용하는 모든 함수는 lpCriticalSection이라는 아큐먼트를 받습니다. 이 lpCriticalSection는 전역변수로 선언되어야 합니다. 왜냐하면 모든 쓰레드에서 같은 값을 참조되어야 하기때문이죠. 즉, 이값을 지역변수로 선언하고 각 함수의 아큐먼트로 넘겨서 사용해서는 안됩니다.
 
크리티컬 섹션의 생성및 초기화를 위해 InitializeCriticalSection()을 선언하고 다 사용한 후에서는 DeleteCriticalSection()으로 할당된 메모리를 제거해야 합니다.
실제로 사용은 EnterCriticalSection()과 LeaveCriticalSection()함수 사이에 위험한 코드(?)를 넣으면 됩니다.
 
다음과 같은 시나리오 일 경우 크리티컬 섹션을 사용할 수 있습니다.
 
전역변수로 선언된 int cnt라는 변수를 쓰레드 1과 쓰레드2가 1씩 증가시키면서 1000회를 반복하고 최종값을 화면에 찍어낸다고 생각해 보죠.
 
#include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; void Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ tmp=cnt; Sleep(1); cnt=tmp+1; } printf("Thread1 End\n"); _endthread(); } void Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ tmp=cnt; Sleep(1); cnt=tmp+1; } printf("Thread2 End\n"); _endthread(); } int main(int argc, char *argv[]) { _beginthread(Thread1, 0, NULL); _beginthread(Thread2, 0, NULL); Sleep(5000); printf("%d\n", cnt); return 0; }
 
위의 경우 여러분은 어떤 결과가 나올 거라고 생각되시나요?
제가 해본 결과  최종 결과 값으로 1000 이 찍혔습니다.
실제로 Thread가 돌았던 갯수만을 생각하면 2000이 되어야 하는데...
이게 바로 쓰레드간의 경쟁(Race Condition)을 벌여서 이런 결과가 나옵니다. 쓰레드간의 Context Switching이 Sleep(1)에서 발생해서 나타나는 현상이죠.
이 소스를 크리티컬 섹션을 사용해서 다음과 같이 바꾸면 정상적으로 2000의 값을 출력합니다.
 
 
#include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; CRITICAL_SECTION crit; void Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ EnterCriticalSection(&crit); tmp=cnt; Sleep(1); cnt=tmp+1; LeaveCriticalSection(&crit); } printf("Thread1 End\n"); _endthread(); } void Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ EnterCriticalSection(&crit); tmp=cnt; Sleep(1); cnt=tmp+1; LeaveCriticalSection(&crit); } printf("Thread2 End\n"); _endthread(); } int main(int argc, char *argv[]) { InitializeCriticalSection(&crit); _beginthread(Thread1, 0, NULL); _beginthread(Thread2, 0, NULL); Sleep(5000); printf("%d\n", cnt); DeleteCriticalSection(&crit); return 0; }
 
생각보다 사용방법이 쉽죠? 위의 소스에서 cnt에 대한 값을 사용하는 부분이 다섯곳이라면 EnterCriticalSection()과 LeaveCriticalSection()함수를 다섯번 사용하면 되겠죠.
만약 cnt와 같이 쓰레드가 경쟁해서 사용해야 하는 자원이 여러개라면 crit변수를 여러개 만들어서 사용하면 됩니다.
 
2. 인터락(InterLock) 함수
 
인터락 함수도 크리티컬 섹션 처럼 어렵지 않습니다. 자신을 갖고 한번 읽어 보세요.
그럼 인터락 함수가 무엇인지 알아보도록 하죠.
 
공유된 변수의 값을 변경하는 경우 CPU는 이것을 한번에 처리하지 않습니다.
즉, cnt++; 라는 구문을 실행하면 CPU는 레지스트리에서 cnt의 현재 값을 빼와서 1을 더한 다음에 다시 레지시트리에 넣는 식으로 되어 있습니다. 이렇다 보니까 쓰레드에의해 실행될 경우 레지스트리에서 값을 빼온 후 변경한 값을 다시 집어 넣기 전에 다른 쓰레드로 스위칭될 수 있습니다.
이렇다 보면 다른 쓰레드에서 이 값을 변경하고 다시 지금의 쓰레드로 돌아 왔을 경우 다른 쓰레드에서 변경한 값이 없어질 수 있는 것이죠. 이런 것을 변수에 값을 대입하는 연산은 원자성(Atomicity)이 없다고 이야기합니다. 아시다 시피 원자는 더 이상 쪼갤 수 없는 것이죠? 원자성이 없다는 이야기는 더 쪼갤 수 있다는 의미가 됩니다. 쪼갤 수 있다는 것은 하나의 연산을 처리하는 중 다른 쓰레드로 변환이 될 수 있다는 이야기죠.
 
C언어로는 cnt++; 와 같이 하나의 문장으로 썼기 때문에 원자성이 있다고 생각되지만... 실제로 CPU는 이를 여러 단계로 처리한다는 것이 인터락함수가 존재하는 이유입니다.
 
위와 같은 이유로 cnt++ 구문을 크리티컬 섹션으로 만들어 주거나 인터락함수를 사용하라는 것이죠.
인터락 함수는 공유된 변수의 값을 여러 쓰레드가 변경해야 하는 경우 인터락함수를 사용하면 원자성을 유지시켜 주겠다는 것입니다.
 
인터락함수에는 다음과 같은 것들이 있습니다.
 
LONG InterlockedIncrement(LONG volatile* Addend);
LONG InterlockedDecrement(LONG volatile* Addend);
LONG InterlockedExchange(LONG volatile* Target, LONG Value);
LONG InterlockedExchangeAdd(LONG volatile* Addend, LONG Value);
PVOID InterlockedExchangePointer(PVOID volatile* Target, PVOID Value);
LONG InterlockedCompareExchange(LONG volatile* Destination, LONG Exchange, LONG Comperand);
PVOID InterlockedCompareExchangePointer(PVOID volatile* Destination, PVOID Exchange, PVOID Comperand);
여기까지는 Windows OS에서 모두 사용할 수 있구요.
아래의 함수들은 Windows Server 2003에서만 사용가능한 함수입니다.
 
LONGLONG InterlockedIncrement64(LONGLONG volatile* Addend);
LONGLONG InterlockedDecrement64(LONGLONG volatile* Addend);
LONGLONG InterlockedExchange64(LONGLONG volatile* Target, LONGLONG Value);
LONGLONG InterlockedExchangeAdd64(LONGLONG volatile* Addend, LONGLONG Value);
LONGLONG InterlockedCompareExchange64(LONGLONG volatile* Destination, LONGLONG Exchange, LONGLONG Comperand);
 
꽤 많죠? 근데 함수명 끝에 64라고 붙은것은 64비트 크기의 변수를 처리한다는 것만 다르므로 몇가지는 설명에서 제외 할 수 있겠네요.
미리 말씀드리는 것은 모든 함수는 변경된 결과의 값을 리턴합니다. 이렇게 하면 리턴값을 이제 설명 안해도 되겠죠? ^^;
 
- InterlockedIncrement() 함수는 지정한 변수의 값에 1을 더합니다. (예 Addend++; 와 같음)
- InterlockedDecrement()함수는 지정한 변수의 값에 1을 감소합니다.  (예 Addend--; 와 같음)
- InterlockedExchange()함수는 Target아큐먼트에 Value를 대입합니다. (예 Target=Value; 와 같음)
- InterlockedExchangeAdd()는 Addend에 Value를 더합니다. (예 Addend+=Value;)
- InterlockedExchangePointer()함수는 Target아큐먼트에 Value(포인터)를 대입합니다. (예 Target=Value;)
- InterlockedCompareExchange()는 Destination의 값이 Comperand와 같으면 Destination에 Exchange값을 대입합니다. (예 if(Destination==Comperand) Destination=Exchange;과 같음)
- InterlockedCompareExchangePointer()함수는 Destination의 값(포인터)이 Comperand와 같으면 Destination에 Exchange값(포인터)을 대입합니다. (예 if(Destination==Comperand) Destination=Exchange;과 같음)
 
이밖에도 Windows Server 2003 이상에서만 사용할 수 있는 다음과 같은 인터락함수가 있습니다.
(사실은 더 많죠. 엄밀히 말하자면 Windows Server 2003 이상에서 많은 인터락함수가 생긴 것 같습니다.)
 
LONG InterlockedDecrementAcquire(LONG volatile* Addend);
LONG InterlockedDecrementRelease(LONG volatile* Addend);
LONG InterlockedIncrementAcquire(LONG volatile* Addend);
LONG InterlockedIncrementRelease(LONG volatile* Addend);
LONGLONG InterlockedExchangeAcquire64(LONGLONG volatile Target, LONGLONG Value);
LONG InterlockedCompareExchangeAcquire(LONG volatile* Destination, LONG Exchange, LONG Comperand);
LONGLONG InterlockedCompareExchangeAcquire64(LONGLONG volatile* Destination, LONGLONG Exchange, LONGLONG Comperand);
LONG InterlockedCompareExchangeRelease(LONG volatile* Destination, LONG Exchange, LONG Comperand);
LONGLONG InterlockedCompareExchangeRelease64(LONGLONG volatile* Destination, LONGLONG Exchange, LONGLONG Comperand);
그런데... 가만히 보면 Acquire와 Release 함수로 되어 있는데... 변수의 변경 순서를 보장한다는 것 같은데... 사실 잘 감이 안가네요. 2003서버가 없어서 테스트 해볼 수도 없고... ㅠㅠ
 
대신 다음의 URL을 참조하세요.
 
 
제가 너무 쉽게 이번장을 넘기려고 하나요? ㅋㅋ 죄송합니다.
지식이 짧다보니...
 
대신 인터락 샘플 소스를 만들어 보여드리죠. 뭐 간단하니깐....
 
#include <stdio.h> #include <process.h> #include <windows.h> int cnt=0; void Thread1(void *arg) { int i, tmp; for(i=0; i<1000; i++){ InterlockedIncrement(&cnt); } printf("Thread1 End\n"); _endthread(); } void Thread2(void *arg) { int i, tmp; for(i=0; i<1000; i++){ InterlockedIncrement(&cnt); } printf("Thread2 End\n"); _endthread(); } int main(int argc, char *argv[]) { _beginthread(Thread1, 0, NULL); _beginthread(Thread2, 0, NULL); Sleep(5000); printf("%d\n", cnt); return 0; }
 
 위의 크리티컬 섹션의 예와 결과나 동작 방법은 같습니다.
 
여기에서 한가지 반듯이 인터락 함수를 사용해야 하는 것은 아닙니다. 이미 보여드린 바와 같이 인터락을 사용해야 하는 곳에 크리티컬 섹션이나 앞으로 이야기할 다른 동기화 방법을 사용해도 됩니다.
 
저는 개인적으로 이번장에서 소개한 방법은 윈도우즈에만 해당되는 것이기 때문에 잘 사용하지 않습니다.
 
Creative Commons License
Creative Commons License이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
Copyright 조희창(Nicholas Jo). Some rights reserved. http://bbs.nicklib.com