본문 바로가기
Programming/C#

C# 멀티스레딩에서 데드락 문제와 해결 방법

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

C# 멀티스레딩에서 데드락 문제와 해결 방법

예제 코드 분석

아래 예제 코드는 두 개의 클래스 LockTestA와 LockTestB를 사용하여 각각 서로의 메서드를 호출합니다. 이 과정에서 데드락이 발생할 수 있습니다.

class LockTestA
{
    static object _lock = new object();
    public static void TestTaskA()
    {
        lock (_lock)
        {
            LockTestB.TestTaskB();
        }
    }
    public static void TestTaskB()
    {
        lock (_lock)
        {
        }
    }
}

class LockTestB
{
    static object _lock = new object();
    public static void TestTaskA()
    {
        lock (_lock)
        {
            LockTestA.TestTaskB();
        }
    }
    public static void TestTaskB()
    {
        lock (_lock)
        {
        }
    }
}

class Program
{
    static void Thread_1()
    {
        for (int i = 0; i < 10000; i++)
        {
            LockTestA.TestTaskA();
        }
    }
    static void Thread_2()
    {
        for (int i = 0; i < 10000; i++)
        {
            LockTestB.TestTaskA();
        }
    }
    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("정상적인 종료 !");
    }
}

데드락의 발생 원인

데드락은 여러 스레드가 서로 상대방이 소유한 락을 기다리며 무한 대기 상태에 빠질 때 발생합니다. 위 코드에서는 다음과 같은 상황이 발생할 수 있습니다:

  1. Thread_1이 LockTestA의 _lock을 획득하고, LockTestB.TestTaskB를 호출하려고 합니다.
  2. 동시에 Thread_2가 LockTestB의 _lock을 획득하고, LockTestA.TestTaskB를 호출하려고 합니다.
  3. 이제 두 스레드는 각각 상대방이 소유한 락을 기다리며 무한 대기 상태에 빠집니다. 즉, 데드락이 발생합니다.

데드락 해결 방법

데드락을 방지하기 위해 다음과 같은 방법을 사용할 수 있습니다:

  1. 자원 접근 순서 정하기: 모든 스레드가 자원에 접근하는 순서를 동일하게 유지하여 데드락을 방지할 수 있습니다.
  2. 타임아웃 설정: 락을 획득하는 시도를 일정 시간 동안만 시도하고, 실패하면 다른 작업을 수행하는 방법입니다.
  3. 락 병합: 가능한 한 락의 개수를 줄여 데드락 가능성을 줄입니다.
  4. 락의 순서를 고정: 여러 개의 락을 사용할 때 락을 획득하는 순서를 고정하여 데드락을 방지합니다.

자원 접근 순서를 고정하여 데드락 해결

자원 접근 순서를 고정하는 방법을 예제 코드에 적용해 보겠습니다.

class LockTestA
{
    static object _lockA = new object();
    static object _lockB = LockTestB._lockB;

    public static void TestTaskA()
    {
        lock (_lockA)
        {
            lock (_lockB)
            {
                LockTestB.TestTaskB();
            }
        }
    }
    public static void TestTaskB()
    {
        lock (_lockA)
        {
        }
    }
}

class LockTestB
{
    public static object _lockB = new object();
    static object _lockA = LockTestA._lockA;

    public static void TestTaskA()
    {
        lock (_lockB)
        {
            lock (_lockA)
            {
                LockTestA.TestTaskB();
            }
        }
    }
    public static void TestTaskB()
    {
        lock (_lockB)
        {
        }
    }
}

위 코드에서는 LockTestA와 LockTestB가 _lockA와 _lockB를 동일한 순서로 획득하도록 합니다. 이렇게 함으로써 데드락을 방지할 수 있습니다.

try{}finally{} 블록과 Monitor 사용

락을 사용하는 동안 예외가 발생하더라도 반드시 락을 해제하도록 보장하려면 try{}finally{} 블록을 사용할 수 있습니다. Monitor 클래스는 lock 구문보다 더 세밀한 제어를 제공합니다.

class LockTestA
{
    static object _lockA = new object();
    static object _lockB = LockTestB._lockB;

    public static void TestTaskA()
    {
        bool lockTaken = false;
        try
        {
            Monitor.Enter(_lockA, ref lockTaken);
            Monitor.Enter(_lockB);
            LockTestB.TestTaskB();
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(_lockB);
                Monitor.Exit(_lockA);
            }
        }
    }
    public static void TestTaskB()
    {
        bool lockTaken = false;
        try
        {
            Monitor.Enter(_lockA, ref lockTaken);
        }
        finally
        {
            if (lockTaken)
                Monitor.Exit(_lockA);
        }
    }
}

이제 Monitor 클래스와 try{}finally{} 블록을 사용하여 락을 제어하고 예외가 발생하더라도 안전하게 락을 해제할 수 있습니다.

데드락은 동시에 접근할 때 발생

class Program
    {
        static void Thread_1()
        {
            for (int i = 0; i < 10000; i++)
            {
                LockTestA.TestTaskA();
            }
        }
        static void Thread_2()
        {
            for (int i = 0; i < 10000; i++)
            {
                LockTestB.TestTaskA();
            }
        }
        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            // 지연 시간
            Thread.Sleep(1000);
            
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine("정상적인 종료 !");
        }
    }

 

Main 중간에 Thread.Sleep(1000)을 추가하였습니다.

컴퓨터 사양에 따라 다르겠지만 1초라는 시간은 스레드가 일을 끝내기에는 충분한 시간입니다.

이처럼 처리하는 시간이 살짝만 어긋나도 데드락은 발생하지 않습니다.

결론

이번 포스트에서는 C#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 데드락 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다.

  1. 데드락 발생 원인: 두 스레드가 서로 상대방이 소유한 자원을 기다리면서 무한 대기 상태에 빠짐.
  2. 자원 접근 순서 정하기: 모든 스레드가 자원에 접근하는 순서를 동일하게 유지하여 데드락 방지.
  3. 타임아웃 설정: 락을 획득하는 시도를 일정 시간 동안만 시도하고, 실패하면 다른 작업 수행.
  4. 락 병합: 가능한 한 락의 개수를 줄여 데드락 가능성 감소.
  5. 락의 순서 고정: 여러 개의 락을 사용할 때 락을 획득하는 순서 고정.
반응형