본문 바로가기
Programming/C#

C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법

by Dev_카페인 2024. 7. 1.
반응형

C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법

예제 코드 소개

아래의 예제 코드는 두 개의 스레드가 number 변수를 각각 증가 및 감소시키는 방식으로 동작합니다. 그러나 동시 접근으로 인해 number 값이 예상과 다르게 나올 수 있습니다.

class Program
{
    static int number = 0;

    static void Thread_1()
    {
        for (int i = 0; i < 10000; i++)
        {
            number++;
        }
    }
    static void Thread_2()
    {
        for (int i = 0; i < 10000; i++)
        {
            number--;
        }
    }
    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);
    }
}

1. 임계영역 (Critical Section)

임계영역은 동시에 접근해서는 안 되는 공유 자원에 접근하는 코드 영역입니다. 임계영역은 한 번에 하나의 스레드만 접근할 수 있도록 해야 합니다. 위 예제에서 number 변수에 대한 접근이 임계영역에 해당합니다.

2. 상호배제 (Mutual Exclusion)

상호배제는 두 개 이상의 스레드가 동시에 임계영역에 들어가지 못하게 하는 기법입니다. 이는 공유 자원의 일관성을 보장하는 데 필수적입니다. 상호배제를 구현하기 위해 C#에서는 lock 구문을 사용할 수 있습니다.

3. Lock

lock 구문은 특정 코드 블록에 대해 한 번에 하나의 스레드만 접근하도록 합니다. lock은 객체를 기반으로 하여 동작하며, 해당 객체가 다른 스레드에 의해 잠겨 있는 동안에는 다른 스레드가 해당 객체에 접근할 수 없습니다.

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

    static void Thread_1()
    {
        for (int i = 0; i < 10000; i++)
        {
            lock (_obj)
            {
                number++;
            }
        }
    }
    static void Thread_2()
    {
        for (int i = 0; i < 10000; i++)
        {
            lock (_obj)
            {
                number--;
            }
        }
    }
    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);
    }
}

이제 number 변수에 대한 접근이 안전하게 보호되며, 최종 출력은 항상 0이 됩니다.

4. 데드락 (Deadlock)

데드락은 두 개 이상의 스레드가 서로 상대방이 소유한 자원을 기다리면서 무한 대기 상태에 빠지는 현상입니다. lock을 잘못 사용하면 데드락이 발생할 수 있습니다. 데드락을 방지하려면 자원 접근 순서를 일관되게 유지하고, 중복 잠금을 피해야 합니다.

5. try{}finally{} 블록

lock 구문은 내부적으로 try{}finally{} 블록을 사용하여 예외가 발생하더라도 락을 해제합니다. 이를 직접 사용할 수 있습니다:

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

    static void Thread_1()
    {
        for (int i = 0; i < 10000; i++)
        {
            try
            {
                Monitor.Enter(_obj);
                number++;
                
                return;
            }
            finally
            {
                Monitor.Exit(_obj);
            }
        }
    }
    static void Thread_2()
    {
        for (int i = 0; i < 10000; i++)
        {
            try
            {
                Monitor.Enter(_obj);
                number--;
                
                return;
            }
            finally
            {
                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);
    }
}

6. Monitor

Monitor 클래스는 lock 구문의 내부적으로 사용하는 클래스입니다. Monitor.Enter는 특정 객체를 잠그고, Monitor.Exit는 잠금을 해제합니다. lock 구문과 달리, Monitor는 잠금 해제 여부를 더 세밀하게 제어할 수 있습니다.

Monitor 사용 예시

위 예제에서 lock 구문을 Monitor로 대체한 것입니다. Monitor.Enter와 Monitor.Exit을 사용하여 명시적으로 잠금을 관리할 수 있습니다.

결론

이번 포스트에서는 C#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 경합 조건 문제와 이를 해결하기 위한 임계영역, 상호배제, lock 구문, 그리고 Monitor 클래스에 대해 알아보았습니다.

멀티스레드 프로그래밍에서 안전하게 공유 자원에 접근하려면 다음 사항들을 고려해야 합니다:

  • 임계영역을 설정하여 동시 접근을 제한합니다.
  • 상호배제를 통해 한 번에 하나의 스레드만 접근하도록 합니다.
  • lock 구문이나 Monitor 클래스를 사용하여 안전한 동기화를 구현합니다.
  • 데드락을 피하기 위해 자원 접근 순서를 일관되게 유지합니다.

이러한 개념을 적용하면 멀티스레딩 환경에서도 안전하고 일관된 프로그램을 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

반응형