BLOG main image





[ 주의사항 ]


criticalsection 사용시 시스템 메모리가 부족하 경우 제대로 동기화되지 않고 오류가 발생한다


이상황을 방지하기위해서 try-catch 를 사용하거나 InitializeCriticalSectionAndSpinCount 함수를 사용해야한다


자세한사항은 :  http://3dmpengines.tistory.com/1168







출저:  http://blog.naver.com/jeon_sw?Redirect=Log&logNo=110013411452 

 

윈도우가 쓰레드의 동기화에 Mutex 와 더불어 CRITICAL_SECTION 이라는 객체를 사용하고 있기 때문에 의미상의 오해가 있을 수 있습니다만, 일반적으로 OS 에서 Crtical Section 이라고 하는 것은 서로 다른 쓰레드나 프로세스가 간섭하지 않고 순차적으로 실행되어야 하는 영역을 뜻하고, 보통 임계 영역 이라고 해석합니다.

제목에 포함된 Critical Section 은 이 영역의 제어를 어떻게 할까를 생각해 보자는 뜻으로 붙인겁니다.

 

Critical Section 의 제어를 어떻게 할지 생각하자?

대부분이 Mutex 나 Semaphore, CRITICAL_SECTION 을 쓰면 되지 않느냐라고 답할겁니다.

 

당연히 그러면 됩니다. 그걸 몰라서가 아니라, 좀 더 성능을 향상하기 위해서는 약간 더 고려해야 될 부분이 있을 거 같아서 입니다.

고려 대상은 주로 여러개의 CPU 를 사용하는 서버의 경우입니다. Uni-Processor 인 일반 PC 에서 실행되는 어플리케이션의 개발은 그냥 기존의 방법을 사용해도 되니까, 이런 어플리케이션을 주로 사용하시는 분은 지금 바로 과감히 다른 페이지나 게시물로 옮겨 가시기 바랍니다.

 

그러면, Multi-Processor 상황에서 어떤 경우를 고려해야 하느냐 하면, Critical Section 이 아주 짧은 경우입니다.

OS에 관한 지식이 약간 있으신 분은 spin-lock 과 Busy-waiting 에 대해 말하고 싶은 거다라는 걸 금방 아셨을 겁니다.

 

spin-lock이 뭔지를 모르시는 분들을 위해 잠시 부가 설명을 하겠습니다.

 

예를 들어 globalCount 라는 공유메모리가(쓰레드간 또는 프로세스간에 공유되는 값) 있다고 가정할 때,

이 값이 순차적으로 증가, 감소하는 값이라고 하면 이 값의 증가에 일반적으로 다음과 같은 방법을 사용합니다.

 

EnterCriticalSection(&cs);

globalCount++;

LeaveCriticalSection(&cs);

 

프로세스간 공유라고 하면,

 

if (WaitForSingleObject(hMutex, millisec) == WAIT_OBJECT_0)

{

    globalCount++;

    ReleaseMutex(hMutex);

}

 

주로 이런 방법을 사용할겁니다. 여기까지 생각이 드셨다면 일반적인 프로그래머라고 생각되고,

 

약간 더 고급 프로그래머라면,

 

InterlockedIncrement(&globalCount);

 

를 생각할 수도 있을겁니다.

 

가장 좋은 방법은 InterlockedIncrement 이지만, 이 함수는 논외로 하겠습니다.

내부적으로 Intel 의 lock inc Instruction-set 을 호출하므로, 이 함수는 정확히 32bit 값에 대해서만 Atomic 처리가 가능하기 때문입니다.(물론 64bit CPU는 64bit입니다만)

지금 예로 든건 편의상 globalCount++ 일 뿐이지, 실제로는 globalCount++; globalCount2--; 일 수도 또는 약간 더 길 수도 있을 겁니다.

 

그럼, 앞의 두가지 경우를 다시 생각해 보겠습니다.

Mutex 는 CRITICAL_SECTION 에 비해 상대적으로 비용이 비싼 객체이므로 우선 대상은 CRITICAL_SECTION 으로 정하겠습니다.

임계영역에서 실행되는 globalCount++; 는 극히 짧은 연산입니다. 이 연산을 위해서 EnterCriticalSection 을 두 쓰레드가 동시에

호출하게 되면, 하나의 쓰레드는 Sleep 상태로 빠지게 될겁니다. 그리고 Sleep 이 되지 않은 쓰레드가 LeaveCriticalSection 을

호출한 후다시 깨어나서 EnterCriticalSection 을 재시도 하여 성공하면 globalCount++ 를 실행하게 됩니다.

이런 상황은 임계영역이 짧은 경우, 하나의 CPU 를 사용하는 PC에서는 거의 발생하지 않습니다. 왜냐하면, 하나의 CPU를 사용하는

시스템에서 동시에 실행되는 작업이란 없기 때문입니다. 임계영역 내에서 Context-Switch 가 발생하지 않는다면, 절대로 일어날

수 없는 상황입니다.

 

그러나, 여러개의 CPU로 동작하는 서버의 경우 실제적으로 여러 쓰레드가 동시에 실행되는 상황이 발생합니다. 그러므로, 아무리

임계영역이 짧아도 이 루틴의 호출빈도가 잦다면, 이런 상황이 자주 발생할 수 있습니다.

그러면, 한 쓰레드가 Blocking 됐다가 깨어나는 작업이 왜 문제가 되느냐?

이 작업이 결코 공짜가 아니라는 데에 문제가 있습니다. 잦은 Context-Switching 은 시스템의 성능을 저하시킵니다.

 

이런 문제를 해결하는 하나의 방법으로 spin-lock 이라는 걸 사용하는 겁니다. 어떻게 동작하느냐 하면,

 

int nSpins;

for (nSpins = spinCount; nSpins > 0; nSpins--)

    if (TryEnterCriticalSection(&cs))

        break;

if (nSpins == 0)

{

    // 처리불가, Sleep 또는 EnterCriticalSection(&cs)); 호출

}

 

와 같은 형태로 구성됩니다. spinCount 횟수만큼 임계영역으로 들어갈 수 있는지를 시도해 보는 겁니다.

들어갈 수 있으면 임계영역의 코드를 실행하고, 들어갈 수 없으면 다시 재시도 합니다.

spinCount는 CPU 갯수 - 1 로 최적화 시키는 것이 일반적일 겁니다.(당연히 CPU가 하나뿐인 시스템에는 적용 안합니다)

이유는 현재 쓰레드가 실행되는 CPU 를 제외한 다른 CPU 가 임계영역을 빠져 나오기를 기다리는 것이 가장 합리적이기 때문입니다.

 

그러므로, 임계영역이 짧은 경우는 일반적인 EnterCriticalSection 보다 이런 형태의 spin-lock 을 사용하는 것이 더 효과적입니다.

CRITICAL_SECTION은 이 처리를 위해 예로 제시한 루프를 돌리지 않아도 윈도우가 자동으로 이 작업을 해 주는 경우가 있습니다.

InitializeCriticalSectionAndSpinCount 로 CRITICAL_SECTION을 초기화할 경우입니다.

그러나, 주의할 점은 임계영역이 긴 경우, 또는 임계영역 내에서 I/O 함수를 호출할 경우, 또는 윈도우 API 를 호출하는 경우 등등의 경우는 이런 방법을 사용하지 않는게 좋습니다.

아니, 사용하지 않는게 좋은게 아니라 절대 사용하면 안됩니다.

 

앞에서도 언급했지만, 숫자값의 연산과 같은 아주 단순한 처리를 행하는 짧은 임계영역을 사용할 때를 대상으로 하는 겁니다.

 

쓰레드간의 공유에 대해서는 TryEnterCriticalSection 이나 InitializeCriticalSectionAndSpinCount 로 이런 문제를 해결할 수 있습니다.

그러면, 프로세스간의 공유에 대해서는 어떻게 될까요?

이 문제를 해결하기 위해서는 WaitForSingleObject(hMutex, 0) 을 대신 사용하는 방법밖에 없습니다.

WaitForSingleObject 는 상당히 덩치가 큰 API 입니다. spin-lock 을 처리하기 위해 이렇게 긴 API 를 호출한다는건 조금 비합리적이지 않을까요?

그래서 윈도우 프로그램에서는 일반적으로 프로세스간의 공유에 있어서는 spin-lock 을 잘 사용하지 않습니다.

 

여기서, POSIX 의 뮤텍스를 잠시 살펴 보면, 뮤텍스 객체가 윈도우와 달리 커널에 존재하지 않습니다. 쓰레드간의 뮤텍스이건 프로세스간의 뮤텍스이건, POSIX 의 뮤텍스는 윈도우의

CRITICAL_SECTION 과 비슷한 형태로 관리되기 때문에 윈도우의 뮤텍스에 비해 훨씬 가볍습니다. 그리고, 이런 형태의 spin-lock 의 처리도 용이합니다.

실제로 Unix 나 Linux 의 어플리케이션은 대부분 락을 걸기 전에 이런 시도를 거치는 경우가 많이 있는데, 윈도우 어플리케이션은 그런 경우를 거의 못본거 같습니다.

윈도우의 경우 클라이언트 어플리케이션이 더 많기 때문일 수도 있을거 같기는 합니다만...

 

윈도우에서도 단순히 spin-lock 을 통해 Busy-waiting 을 수행할 경우는 OS 의 지원을 받을 필요가 없기 때문에 커널 객체 없이 사용자모드 자체로 구현이 가능합니다.

그래서, 이런 기능을 수행하는 함수를 만들어 봤습니다.

 

void light_lock(long* _lock)

{

    int a;

    for (;;) {

        __asm {

            mov edx, _lock;

            cmp dword ptr[edx], 0;

            jne retry;

            mov eax, 1;

            xchg eax, dword ptr[edx];

            cmp eax, 0;

            jne retry;

        }

        break;

retry:

        __asm {

            pause;

        }

    }

}

 

void light_unlock(long* _lock)

{

    *_lock = 0;

}

 

쓰레드간의 동기화에 있어서는 long 타입의 변수 하나를 선언하고 초기값을 0 으로 설정합니다.

light_lock 을 수행해 성공한 쓰레드는 이 변수를 1 로 설정하고 임계영역으로 들어 갑니다.

그렇지 못한 쓰레드는 pause Instruction 을 호출하여 잠시 대기한 후 다시 시도합니다.

pause 는 펜티엄4 에서 새롭게 기능이 개선된 명령입니다. 이 명령을 수행함으로써 CPU 에게 spin-lock 을 수행한다는 힌트를 주게 됩니다.

 

프로세스간의 동기화에 있어서는 역시 long 타입의 변수를 공유메모리로 선언합니다. 윈도우는 공유메모리가 파일맵핑 뿐이므로 파일맵핑

메모리로 선언하여 위의 함수를 수행하면 됩니다.

 

Mutex 와는 비교가 안되고, CRITICAL_SECTION 보다도 더 가볍기 때문에 단순한 동기화가 필요한 경우 활용가치가 높을 거라 생각됩니다.

물론 Busy-waiting 이니까 절대 대기시간이 길지 않은 작업에만 사용해야 겠지만...

아직 실무 적용을 한 소스는 아니고, 일단 시스템 성능 향상을 위해 나름대로 생각해 본 방법입니다.

반응형

+ Recent posts