Critical Section은 병렬컴퓨팅에서 둘 이상의 스레드가 동시에 접근해서는 안되는 공유 자원을 접근하는 코드를 말한다. -위키백과-

즉, 공유 자원을 접근하는 코드를 Critical Section이라고 한다.

Critical Section을 구현하는 여러가지 방법이 있는데 그 중 가장 대표적인 Spinlock을 오늘 구현해보도록 하겠다.


SpinLock을 쉽게 설명하자면, 어떤 사람이 화장실을 문을 잠그고 볼일을 보는 동안, 문 앞에서 무작정 기다리다가 사람이 나오면 그때 들어가서 문을 잠그는 방식이다.

무지성 기다림 -> 사람 나옴 -> 문 들어가서 잠금 으로 간단하게 이해할 수 있다.

직관적으로 다음과 같이 SpinLock을 구현할 수 있을 것 처럼 보인다.

class SpinLock // Lock이 풀릴 때 까지 돌아가는 존버메타
        // 화장실을 사용하는 사람이 문을 열고 나올때 까지 앞에서 기다리는 중 ㅠㅠ

    {
        volatile bool _locked = false; // 가시성을 보장해주겠다. 

        public void Acquire()
        {
            while (_locked)  // 화장실 문이 비어있을 때 까지 대기를 한다. 
                // _locked이 false일때 빠져나간다. 
            { }

            // 들어가서 문을 잠그는 작업 
            _locked = true; 

        }

        public void Release()
        {
            _locked = false;
        }
    }

    class Program
    {
            

        static int number = 0;
        static SpinLock _lock = new SpinLock(); 

        static void Thread_1()
        {
            for(int i=0; i<10000; i++)
            {
                _lock.Acquire();

                number++;

                _lock.Release();
            }
        }

        static void Thread_2()
        {

            for (int i=0; i<10000; i++)
            {
                _lock.Acquire();

                number--;

                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(number);

        }
    }

먼저, volatile 키워드로 _locked 변수의 가시성을 보장해준다.

Acquire 함수는 화장실에서 무작정 기다리는 상황을 구현한 함수이다.

_locked이 true일 때 즉, 다른 쓰레드에서 작업을 할 때 (화장실을 쓸 때)는 무작정 기다리다가, _locked이 false로 바뀌면 그때 while문을 빠져나온다.

그 다음 _locked을 true로 바꿔서 다른 쓰레드가 공유 자원에 접근하지 못하도록 잠군다.


작업이 다 끝났으면 Release 함수를 실행한다. 이는 볼일을 마치고 화장실을 나가는 상황이다. _locked을 false로 바꿔서 다른 쓰레드가 공유 자원에 접근할 수 있도록 해준다.


자 이렇게만 놓고 보면 아무런 문제가 없을 것 같다. 하지만 콘솔창을 확인해보자. 원래대로라면 0이 출력되어야 한다.

또 시작이다. 멀티 쓰레드 환경에서 우리가 무엇을 놓쳤길래 이런 결과가 나올까? 바로 원자성이다.


아주 미세한 순간이지만, 화장실 문 앞에서 기다리는 과정과 문이 열릴 때 들어가서 문을 잠그는 작업 사이에 시간 차가 있다.

만약 그 미세한 순간에 다른 쓰레드가 접근한다면? 화장실에 두 쓰레드가 들어가는 상황이 발생한다. 그런 상황에서는 number에 무슨 값이 들어있는지를 예측할 수 가 없다.

구체적으로 얘기하자면, number++와 number-- 연산은 우리 눈에는 하나의 연산으로 보이지만 기계어(어셈블리 언어)의 관점에서는 3단계의 연산으로 이루어졌다.
Race Condtion과 Interlocked을 공부할 때 배웠다. ㄷㄷ

예를 들어 number++ 라는 연산은 CPU 레벨에서

  1. number의 주소에 저장된 값을 가져와서

  2. 그 값에 1을 더해주고

  3. 더해준 값을 다시 number에 대입한다.

의 3단계로 이루어 진다. number-- 도 마찬가지다.

멀티 쓰레드 환경에서는 쓰레드들의 경합에 의해서 마지막 단계 즉, number에 대입하는 단계에서 엉뚱한 값이 대입할 수도 있는 것이다.

따라서 문제를 해결하기 위해서는 화장실 문 앞에서 기다리는 과정과 문을 열고 잠그는 과정을 한 번에 실행하여 number에 작업할 때 하나의 쓰레드만 작업할 수 있게 해야한다.


      public void Acquire()
        {

            while (true)
            {
                int expected = 0;
                int desired = 1;

                //Compare and Swap(CAS)
                int original = Interlocked.CompareExchange(ref _locked, desired, expected);
                // _locked 가 comparand(expected)와 같다면 _locked에 desired을 대입
                //  original은 원래 _locked이 가지고 있던 값. 

                if (original == expected)
                {
                    break;
                }
            }

        }

_locked 변수를 int형 변수로 만들어주고 Acquire 함수를 다음과 같이 수정해보자.

Interlocked.CompareExchange 함수는 _locked와 expected 의 값이 같으면, 원자성을 보장하면서 _locked 에 desired 값을 할당한다.

위에서 우리가 _locked이 false로 변해서 while을 빠져나오는 것과 _locked에 true를 할당하는 과정이 원자성이 보장이 안된 것에 반해

Interlocked을 사용하여 비교와 대입을 한 번에 할 수 있다.

이 방법을 Compare and Swap 줄여서 CAS라고 한다.

number에 0이 나오는 것을 확인할 수 있다.

profile
POSTECH EE 18 / Living every minute of LIFE

0개의 댓글