공부/인프런 - Rookiss

Part 4-2-7. 멀티쓰레드 프로그래밍 : Lock 기초

셩잇님 2023. 9. 12. 18:38
반응형

 

 

멀티 쓰레드

 

Lock (OS에서는 크리티컬 섹션(CriticalSection), C++에서는 std::mutex 라고 불린다.)

 

 Interlocked 계열 메서드는 성능도 빠르고 좋긴 하지만, 단점이 존재한다. 바로 정수만 사용할 수 있는 것이다. 우리가 나중에 멀티 쓰레드를 이용해 프로그램을 짤 때에는 단순히 number++ 만을 하지는 않을 것이다. 따라서 특정 신호를 주어서 사용자가 정한 블록 안의 내용은 하나의 쓰레드만 실행하도록 제어할 수 있는 도구가 필요할 것이다.

 

 먼저 소스코드 내 가상의 선을 긋는다고 생각하자. 이 영역은 아무도 접근할 수 없으며, 내가 먼저 점유할 경우 다른 쓰레드는 얼씬도 하지 못한다. 라고 생각하자.

 

class Program
{
    static int number = 0;
    static object _obj = new object();

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            Monitor.Enter(_obj);

            number++;

            Monitor.Exit(_obj);
        }
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            Monitor.Enter(_obj);

            number--;

            Monitor.Exit(_obj);
        }
    }

    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);
    }
}

 

 이럴 때 사용하는 것이 바로 Monitor 메서드이다. 이를 비행기 화장실로 생각해보자. 우리는 비행기를 타면 화장실을 사용할 수 있는데 대게 화장실은 1인용이다. 따라서 사용하기 위해서는 문을 잠궈야한다. 이 역할을 하는 것이 바로 Monitor.Enter() 메서드이다. 즉 내가 쓰레드 1에서 문을 잠군다면, 쓰레드 2에서는 문을 잠그려고 하기 전에, 이미 문이 잠겨있기 때문에 문이 열리기까지 대기해야 한다.

 

 따라서 문을 잠고 볼일(쓰레드 1의 number++)을 다 보고 문을 열어주는 것과 같다. (Monitor.Exit()) 반대로 쓰레드 2가 먼저 시작한 경우, 쓰레드 1의 Enter는 쓰레드 2의 Exit이 끝나고 난 이후에 실행된다. 이러한 상황을 상호 배제(Mutual Exclusive) 라고 한다. 나만 사용할 거야! 너 배제* 할거야! (❗❓ 정처기 교착상태(=데드락), 상점비환 (=상호배제, 점유 및 대기, 비선점, 환형대기)의 개념이 또 나온다.) 

* 배제 = 물리쳐서 제외하는 것.

 

 그러나 Monitor Enter, Exit도 단점이 없는 것은 아니다. 바로 관리하기가 어려워 진다는 것이다. 만약 쓰레드 1 내부에서 Exit 메서드를 정상적으로 실행하지 않고 return; 을 실행해버릴 경우, 프로그램은 무한루프에 빠지는 것이다. 비행기 화장실로 비유하면 화장실에 들어간 사람이 그만 화장실에서 잠이 든 셈이고, 이 사람이 나오기를 기다리고 있던 사람들(쓰레드 2)은 하염없이 화장실을 다 쓰고 나오기만을 기다리고 있는 것이다. 이를 교착상태(=데드락)이라고 한다.

 

 그렇다면 우리가 Enter와 Exit의 짝을 모두 다 맞췄고, number가 10000이 될 경우 쓰레드를 종료한다고 가정해보자.

 

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                Monitor.Enter(_obj);

                number++;

                if (number == 10000)
                {
                    Monitor.Exit(_obj);
                    return;
                }

                Monitor.Exit(_obj);
            }
        }

 

 벌써부터 코드가 더러워 지는 것을 알 수 있다. 특정 조건, 상황마다 Monitor.Exit(_obj);코드를 일일히 작성해야 하기 때문이다. 심지어 number를 0으로 나누면(divide by zero) 정상적으로 Monitor.Exit(_obj);를 실행하지 않고 문제가 생길 수도 있는 것이다. 따라서 Monitor.Enter/Exit을 사용하려면 try ~ catch문을 사용해야 한다.

 

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {

                try
                {
                    Monitor.Enter(_obj);
                    number++;
                    return;
                }
                finally
                {
                    Monitor.Exit(_obj);
                }
            }
        }

 

 물론 이렇게 진행한다고 해도 모든 경우를 다 try ~ catch를 사용해야 하기 때문에 번거로운 것은 Monitor.Exit 메서드와 동일한 셈이다. 그래서 이를 해결하기 위해 lock 키워드를 사용한다. 

 

        static void Thread_1()
        {
            lock(_obj)
            {
                number++;
            }
        }

        static void Thread_2()
        {
            lock (_obj)
            {
                number--;
            }
        }

 

 lock 메서드를 이용해 사용 할경우 try ~ catch와 똑같은 역할을 하고, 내부 구현도 Moniter.Enter(), Exit()으로 이루어져 있다. 그렇지만 이를 보다 편리하게 사용할 수 있도록 관리하는 것이다. lock 메서드를 이용하면 본인이 알아서 잠구고, 알아서 열어준다. 👍

 

 

 

반응형