반응형
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("정상적인 종료 !");
}
}
데드락의 발생 원인
데드락은 여러 스레드가 서로 상대방이 소유한 락을 기다리며 무한 대기 상태에 빠질 때 발생합니다. 위 코드에서는 다음과 같은 상황이 발생할 수 있습니다:
- Thread_1이 LockTestA의 _lock을 획득하고, LockTestB.TestTaskB를 호출하려고 합니다.
- 동시에 Thread_2가 LockTestB의 _lock을 획득하고, LockTestA.TestTaskB를 호출하려고 합니다.
- 이제 두 스레드는 각각 상대방이 소유한 락을 기다리며 무한 대기 상태에 빠집니다. 즉, 데드락이 발생합니다.
데드락 해결 방법
데드락을 방지하기 위해 다음과 같은 방법을 사용할 수 있습니다:
- 자원 접근 순서 정하기: 모든 스레드가 자원에 접근하는 순서를 동일하게 유지하여 데드락을 방지할 수 있습니다.
- 타임아웃 설정: 락을 획득하는 시도를 일정 시간 동안만 시도하고, 실패하면 다른 작업을 수행하는 방법입니다.
- 락 병합: 가능한 한 락의 개수를 줄여 데드락 가능성을 줄입니다.
- 락의 순서를 고정: 여러 개의 락을 사용할 때 락을 획득하는 순서를 고정하여 데드락을 방지합니다.
자원 접근 순서를 고정하여 데드락 해결
자원 접근 순서를 고정하는 방법을 예제 코드에 적용해 보겠습니다.
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#에서 멀티스레딩 프로그래밍 시 발생할 수 있는 데드락 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다.
- 데드락 발생 원인: 두 스레드가 서로 상대방이 소유한 자원을 기다리면서 무한 대기 상태에 빠짐.
- 자원 접근 순서 정하기: 모든 스레드가 자원에 접근하는 순서를 동일하게 유지하여 데드락 방지.
- 타임아웃 설정: 락을 획득하는 시도를 일정 시간 동안만 시도하고, 실패하면 다른 작업 수행.
- 락 병합: 가능한 한 락의 개수를 줄여 데드락 가능성 감소.
- 락의 순서 고정: 여러 개의 락을 사용할 때 락을 획득하는 순서 고정.
반응형
'Programming > C#' 카테고리의 다른 글
C# 스레드 제어 Thread.Sleep(0), Thread.Sleep(1), Thread.Yield()의 차이점 (0) | 2024.07.01 |
---|---|
C# SpinLock 효율적인 스레드 동기화 기법 (0) | 2024.07.01 |
C# 멀티스레딩에서 안전한 공유 자원 관리를 위한 임계영역과 Lock 사용법 (0) | 2024.07.01 |
C# 멀티스레딩에서 경합 조건과 Interlocked 클래스의 사용 (0) | 2024.07.01 |
멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법 (0) | 2024.07.01 |