Win32 API

쓰레드의 임계 영역(Critical Section)에서 발생할 수 있는 이슈

FreeChild 2010. 4. 2. 16:16

Windows 운영체제에서 일반적으로 쓰레드가 프로그램의 기본 실행 단위입니다.

쓰레드가 프로그램의 기본 실행 단위라는 말은 모든 프로세스는 쓰레드를 가지고 있음을 의미합니다. 

프로그램의 실행 과정을 간략히 살펴보면 실행된 프로그램은 로더의 의해서 메모리에 적재되고, 커널이

실행된 프로그램을 상주시키기 위한 하나의 프로세스를 생성시켜 주게 됩니다. 프로세스가 생성후, 메인
 
쓰레드를 하나 생성하고 그 메인 쓰레드가 main() 함수(Entry Point)를 실행하게 되는 것입니다.

(여기서 프로세스와 쓰레드는 커널에 의해 생성되고 커널 오브젝트에 의해 관리되어 집니다)

다시 한번 더 정리하자면,

기본적으로 모든 프로세스는 최소한의 메인 쓰레드를 가지고 있다는 뜻입니다. 

쓰레드는 생성시, 독립적인 메모리 공간(Stack 영역)을 가지며 프로세스 내의 나머지 외부 영역

(Global)은 (다른 쓰레드와) 공유하게 됩니다. 외부 영역을 공유하게 됨으로써, 쓰레드간의 편리하게

통신할 수 있다는 점이 장점이 될 수도 있습니다. 하지만, 공유된 메모리 영역을 두 개 이상의 쓰레드가

접근할 경우에 문제가 될 수 있다는 것이 바로 문제의 핵심입니다.

이렇게 두 개 이상의 쓰레드에 의해서 같은 공유 메모리 영역에 접근하면서 생길 수 있는 문제 구역 즉,

다시 한번더 정리하자면 두 개 이상의 쓰레드에 의해서 접근되면 안되는 메모리 영역을 두고 임계 영역

(Critical Section)이라고 합니다. 프로세스 내의 쓰레드들은 동시에 실행되는 것이 아니라 일정한

CPU Time을 조금씩 조금씩 나눠서 할당받기 때문에 실제 활성화(Activation)되는 쓰레드는 하나뿐입

니다. 하지만, 해당 영역이 문제가 되는 것은 확실합니다.

문제가 되는 이유를 아주 간단한 예를 들어, 설명해 드리겠습니다.

우선 A, B 두 개의 쓰레드가 있으며 두 개의 쓰레드가 전역으로 선언된 변수를

같이 사용하고 있다고 가정하겠습니다.

 

int shared_var = 0;     // UnInitialized Data Segment - BSS 영역

DWORD WINAPI Thread_A(void *arg)
{
    shared_val = shared_val + 5;

DWORD WINAPI Thread_B(void *arg)
{
    printf( "%d \n", shared_val );
}

 

위의 예제를 보시면 아시겠지만, A 쓰레드는 전역 변수 shared_val을 5씩 증가시켜 주는 역할을 하며,

B 쓰레드는 전역 변수 shared_val을 연속적으로 출력해주는 역할을 하는 쓰레드입니다.

(이 프로그램의 의도는 연속적으로 계속 5씩 증가하는 결과를 보여주는 프로그램입니다)

여기서 반드시 정확하게 이해하고 있어야 하는 것은 연산되어지는 과정입니다.

shared_val = shared_val + 5;를 연산하기 위해서 우선 r-value의 첫번째 shared_val 을 임시 메모리
 
공간에 할당하게 됩니다. 그리고 5를 다른 임시 메모리 공간에 할당하게 됩니다. 할당된 두 임시 메모리

공간에서 각각의 값을 읽어와 연산을 하고 연산된 결과 값을 리턴하여 l-value의 shared_val에 저장하

게 됩니다.

제가 위에서도 말씀드렸지만, 쓰레드는 분명 Cpu Time을 할당받는다고 하였습니다. A 쓰레드가 어떤

작업을 수행하고 있는데 CPU Time을 초과하게 되면 B 쓰레드에게 CPU Time이 할당되게 됩니다.

그러니까 A 쓰레드가 수행중인 작업을 끝내든 못 끝내든 상관없이 말입니다.

문제는 이겁니다. 만약에 아까 코드에서 A 쓰레드가 shared + 5를 연산하기 위해 임시 메모리에 할당한

후, 각각 값을 메모리에 저장한 상태에서 cpu time이 끝난다면 어떻게 될까요? 연산까지는 하지 못한

상태에서 말이죠. (※ cpu time은 각 쓰레드에게 주어지는 시간은 분명 일정하지만 프로세스도 마찬가

지로 cpu time을 할당받는데 프로 세스가 받는 cpu time은 사용자나 외부 환경에 의해서 일정치가 않

습니다)

일단, 임시 메모리에 저장한 값은 다른 임시 메모리 영역으로 옮겨 놓고, B 쓰레드를 호출하게 되는데,

B 쓰레드가 하는 역할은 전역 변수 shared_val의 값을 출력하는 것입니다. 바로 이곳이 문제가 되는 부

분입니다. 전역 변수 shared_val의 값은 A 쓰레드가 연산하기 직전에 cpu time을 B 쓰레드에게 넘겨

주었는데, B 쓰레드는 A 쓰레드가 한 작업에 상관없이 무조건 출력을 하게 되는데, 결과값은 증가되지

않는 값을 출력하는 것이 문제점입니다.

사실, 지금과 같은 예제로 인한 Critical Section 접근 문제는 거의 희박합니다. 하지만, B 쓰레드가 단

순한 출력이 아니라, A 쓰레드보다 더욱 복잡한 연산을 수행하며, 두 개의 쓰레드가 아닌 여러개의 쓰레

드가 접근할 때는 문제가 심각해 질 수 있다는 것을 말씀드리고 싶었습니다. 실제 임계 영역을 해결해야

하는 문제는 프로그래밍 중에 꾀나 빈번히 발생합니다.

어디까지나 제가 보여드린 예제는 모의적인 코드에 불과하며, 충분히 문제가 불거질 수가 있다는 것을

이해하시면 됩니다. Critical Section Problem을 해결하기 위해서는 Critical Section을 공유하고 있는
 
쓰레드에 대해서 동기화(Synchronization)를 해야 합니다. 동기화 기법은 크게 User Mode

(CRITICAL_SECTION OBJECT)와 Kernel Mode(Mutex, Semaphore, Event 등)에서의 동기화

기법이 있는데, 운영체제 관련 자료를 참고하시기 바랍니다.

Written By Sim-Hyeon, Choe