공부/인프런 - Rookiss

Part 4-2-12. 멀티쓰레드 프로그래밍 : 이벤트(AutoResetEvent, ManualResetEvent), 뮤텍스

셩잇님 2023. 9. 16. 22:08
반응형

 

 

멀티 쓰레드

 

이벤트(AutoResetEvent)

 

 오늘의 주제는 이벤트를 이용한 락 구현이다. 이전 시간에서 직원을 새로 고용해서 직원에게 부탁하는 방법이 이에 해당한다. 그렇지만 사실 해당 직원은 식당 직원이 아닌 커널 레벨에 있는 식당 관리자에 해당하는 것이다 😲.. 이런 식으로 커널 레벨로 옮겨서 실행을 할 때에는 어마어마하게 느리다는 단점이 있다. 대신 본인 입장에서는 시간을 낭비하는 것이 아니므로 행동을 이어가는 장점이 있다.

 

 C#에서는 이벤트를 구현할 때에는 두 가지 방법이 있다. 

 

1. Auto Reset Event

 이는 톨케이트를 생각하면 된다. 톨게이트를 보면 차가 한대한대씩 지나가고, 지나갈 때마다 톨게이트가 닫혀 대기해야 하는 상황이 발생하는데 이것이 바로 Auto Reset Event의 개념이다.

 

2. Manual Reset Event

 이는 방문과 같은 개념이다. 문이 수동으로 잠기기 때문에 문을 열고 나간 상태에서 문을 닫지 않는다면 누구든, 아무나 신나게 문을 통과할 수 있는 개념이다.

 

위 두 가지의 방법을 소스 코드로 구현해보자.

 


 

 

Auto Reset Event

 

class Lock
{
    AutoResetEvent _available = new AutoResetEvent(true);



    public void Acquire()
    {
        _available.WaitOne(); // 입장을 시도한다.
        // _available.Reset(); // bool을 false로 변경하는 개념이지만, WaitOne에 포함되어 있는 개념이다.
    }

    public void Release()
    {
        _available.Set(); // 퇴장 bool의 값을 true로 변경한다.
    }
}

class Program
{
    static int _num = 0;
    static Lock _lock = new Lock();

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num++;
            _lock.Release();
        }
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num--;
            _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(_num);
    }
}

 

 위 소스코드는 Auto Reset Event를 구현한 코드이다. AutoResetEvent 클래스를 new를 이용하여 만들어 주면 되는 데 이 때 bool 값을 넣어준 상태에서 생성해주어야 한다. 여기에서 사용되는 bool의 값은 위에서 설명해준 것과 같이 커널 단위 까지 내려가서 실행하는 것을 의미한다.

 

 인자 값으로 받는 true와 false의 값은 다음과 같다. true 값은 들어올 수 있는 상태이며, false의 값은 들어올 수 없는 상태이다. 즉. 톨게이트를 열어놓은 상태인지, 닫아놓은 상태인지 구분하는 인자값이다. 또한 AutoResetEvent의 특징으로는 Auto가 들어가는 만큼 문을 닫는 행동을 자동으로 해준다는 것이다. 

 

 이제 코드 작성을 다 했으니 프로그램을 실행해보자. 으잉.. 🤦‍♂️ 또 실행해도 결과 값이 나타나지 않는다. 왜냐하면 해당 이벤트는 커널 단위까지 내려가서 실행하기 때문에 작업이 매우 오래걸린다. 따라서 for문 내의 값을 10,000회로 수정하면 된다. 😁

 

 


 

 

Manual Reset Event 

 

class Lock
{
    ManualResetEvent _available = new ManualResetEvent(true);

    public void Acquire()
    {
        _available.WaitOne(); // 입장 시도


        _available.Reset(); // 문을 닫는다.

        // 사실은 이벤트를 사용하는 방법 말고도
        // 커널을 이용해 순서를 맞추는 방법도 있다.
        // 뮤택스이다.
    }

    public void Release()
    {
        _available.Set(); // 문을 열어준다.
    }
}

class Program
{
    static int _num = 0;
    static Lock _lock = new Lock();

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num++;
            _lock.Release();
        }
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num--;
            _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(_num);
    }
}

 

 ManualResetEvent도 해당 클래스를 이용하여 AutoResetEvent 와 마찬가지로 생성해주어 사용하면 된다. 하지만 AutoResetEvent와 다른 개념이 하나 있는데, ManualResetEvent는 방문과 같아서 문이 열려있으면 통과시켜 주지만 AutoResetEvent와 달리 자동으로 문을 닫지 않는다. 따라서 Reset 메서드의 기능이 Waitone에 빠져있는 것이다. 그럼 뭐 Reset 메서드를 호출해주면 되는거 아닐까?

 

 

 당연히 아니다. 뭐가 문제일까? ㅋㅋㅋ 사실 계속해오던 문제이다. 입장을 시도하고 문을 닫는 행위가 원자성이 보장되는 것이 아닌 두 개로 나뉘어져 있기 때문에 또 문제가 된다. 결국 락을 구현할 때에는 ManualResetEvent 하는 것보다 AutoResetEvent를 사용하는 것이 좋다.

 

 그렇다면 언제 ManualResetEvent를 사용해야 할까? 생각해보면 경우에 따라 한번에 하나만 입장할 때가 아닌 _available의 값을 false로 설정한 다음에 어떤 작업이 끝난 경우 (패킷 전송과 화면 로딩 등) 모든 쓰레드들이 다시 동작하는 상황을 만들어 줄 때 ManualResetEvent를 사용하는 것이 좋다. 🤯..

 

 


 

 

짜잔~ 또 다른 방법이 있습니다.

 

사실은 이벤트를 사용하는 방법 말고도 커널을 이용해 순서를 맞춰주는 방법 또한 존재한다. 바로 이것이 뮤텍스(Mutex)이다. 소스 코드를 통해 뮤텍스는 어떻게 생성하고, 사용하는지 확인해보자.

 

class Program
{
    static int _num = 0;
    static Mutex _lock = new Mutex();

    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.WaitOne();
            _num++;
            _lock.ReleaseMutex();
        }
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.WaitOne();
            _num--;
            _lock.ReleaseMutex();
        }
    }

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

 

 뮤텍스는 이전의 Lock과 같은 새로운 클래스를 만들 필요가 없이 Program 클래스 내부에서 생성하고 실행해주면 된다. 그렇지만 실행해보면 뮤텍스는 스핀락에 비해 느린 것을 알 수 있다. 왜 뮤텍스는 스핀락에 비해 느릴까? 바로 뮤텍스 또한 이벤트와 같은 개념인 커털 동기화 객체이기 때문이다. 

 

 예를 들어 설명하자면 식당에서 일하는 직원끼리 식당 문제를 해결하면 참 좋겠지만, 때로는 문제가 커져 식당 매니저의 개입이 필요한 상황이라고 보면 된다. 따라서 매니저인 뮤텍스가 직원들의 일을 조율하고, 문제를 해결하는 상황인 것이다.

 

 그렇다면 뮤텍스와 AutoResetEvent의 차이점은 무엇일까? 둘이 하는 일은 비슷하지만 뮤텍스는 AutoResetEvent에 비해 조금 더 많은 정보를 소유하고 있다. 예를 들면 락이 몇 번 진행되었는지 카운팅 하는 기능도 들고 있으며, 쓰레드의 ID 정보와 같은 정보를 가지고 있어 해당 쓰레드가 락을 걸었는지, 락을 해제할 때 정상적으로 해당 쓰레드를 해제하는지, 다른 쓰레드를 해제하는지 등의 정보를 알 수 있다. 따라서 AutoResetEvent에 비해 실행이 무겁다. 

 

 

 

반응형