2. C# 비동기 프로그래밍 : async, await 이해
3. C# 비동기 프로그래밍 : Task의 연속 작업 (Continuations) 이해
4. C# 비동기 프로그래밍 : Thread와 Task의 차이점
5. C# 비동기 프로그래밍 : deadlock 문제와 해결 방법
이번 포스팅은 Deadlock에 대해 정리한 내용을 알아보겠습니다.
실제로 비동기 프로그래밍으로 프로그램을 짜게 되면 생각보다 구조 설계가 힘들다고 느낌을 받을 수도 있습니다.
그런 느낌을 받게 하는 문제인 데드락(Deadlock)에 대해 정리하였습니다.
| DeadLock 문제 이해
먼저 데드락(Deadlock)이란 두 개 이상의 프로세스나 스레드가
상호 배재적으로 사용하고 있는 자원을 요청하면서 서로가 가진 자원을 대기하는 현상을 말합니다.
이로 인해 프로그램이 계속 진행되지 않고 멈추게 됩니다.
C#에서 데드락 문제는 주로 다중 스레드 환경에서 발생합니다.
두 개 이상의 스레드가 각각 다른 자원을 잠금 상태로 만든 후,
다른 스레드가 가지고 있는 자원을 요청하는 상황에서 발생할 수 있습니다.
예를 들어 Thread A와 Thread B가 있을 때, A가 자원 1을 잠그고 B가 자원 2를 잠글 수 있습니다.
이후 A가 자원 2를 B가 자원 1을 요청하면 서로가 자원을 기다리게 되어 데드락 상태에 빠지게 됩니다.
| DeadLock 문제 해결
앞서 설명한 데드락 문제를 해결하기 위한 몇 가지 방법이 있습니다.
- Lock Ordering : 자원에 대한 접근을 순차적으로 진행
즉, 하나의 스레드만 자원에 접근하도록 설정하는 것이지만 병렬 처리의 장점을 살리지 못한다는 단점이 있습니다. - Lock Timeout : 일정 시간 동안 자원의 잠금을 획득하지 못할 경우, 작업 취소 후 잠금 해제하는 방법
- Deadlock Avoidance Algorithms : 데드락을 감지하는 알고리즘을 사용
데드락을 감지하지만 해결 비용이 크다는 단점이 있습니다.
실제로 C#에서의 코드 예시를 통해 데드락 해결 방법에 대해 알아보겠습니다.
| 첫 번째 : lock 키워드를 사용한 데드락 방지
예시) 데드락 발생 코드
private Object lock1 = new Object();
private Object lock2 = new Object();
void Method1()
{
lock (lock1)
{
// lock1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
lock (lock2)
{
// lock2에 대한 작업 수행
}
}
}
void Method2()
{
lock (lock2)
{
// lock2에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
lock (lock1)
{
// lock1에 대한 작업 수행
}
}
}
위 코드에서 Method1과 Method2를 동시에 실행하면 데드락이 발생합니다.
Method1이 lock1을 잡고 lock2를 기다리는 동안, Method2는 lock2를 잡고 lock1을 기다리기 때문입니다.
이 문제를 해결하려면 자원에 접근하는 순서를 정해주는 것이 좋습니다.
예시) lock을 통해 자원에 순서를 접근을 조정
void Method1()
{
lock (lock1)
{
// lock1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
lock (lock2)
{
// lock2에 대한 작업 수행
}
}
}
void Method2()
{
lock (lock1)
{
// lock1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
lock (lock2)
{
// lock2에 대한 작업 수행
}
}
}
이제 Method1과 Method2는 항상 lock1을 먼저 잡고, 그다음에 lock2를 잡습니다.
이렇게 하면 두 메서드가 동시에 실행되더라도 데드락이 발생하지 않습니다.
| 두 번째 : Monitor 클래스를 사용한 데드락 방지
lock 키워드와 유사한 기능을 제공하지만, 더 많은 제어를 가능하게 합니다.
Monitor.Enter와 Monitor.Exit 메서드를 사용하여 임계 영역을 생성합니다.
예시) Monitor를 사용한 데드락 방지
object lock1 = new object();
object lock2 = new object();
void Method1()
{
Monitor.Enter(lock1);
try
{
// lock1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
Monitor.Enter(lock2);
try
{
// lock2에 대한 작업 수행
}
finally
{
Monitor.Exit(lock2);
}
}
finally
{
Monitor.Exit(lock1);
}
}
void Method2()
{
Monitor.Enter(lock1);
try
{
// lock1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
Monitor.Enter(lock2);
try
{
// lock2에 대한 작업 수행
}
finally
{
Monitor.Exit(lock2);
}
}
finally
{
Monitor.Exit(lock1);
}
}
이전의 lock 키워드를 사용한 코드와 동일한 동작을 수행하지만 Monitor.TryEnter 메서드를 사용하면 일정 시간 동안 잠금을 시도하고, 그 시간 동안 잠금을 얻지 못하면 취소하는 등의 추가적인 제어가 가능합니다.
| 세 번째 : Mutex 클래스를 사용한 데드락 방지
C#에서 Mutex는 다른 프로세스오하 공유될 수 있는 동기화 기본 요소입니다.
Mutex는 한 번에 하나의 스레드만이 자원에 접근할 수 있도록 제한하며, 다른 스레드는 자원이 사용 가능해질 때까지 기다리게 됩니다. 앞서 설명한 lock과 Monitor를 사용하는 방법과 유사하며 주요 차이점은 Mutex는 시스템 범위에서 작동하고 이름을 가질 수 있으며, 다른 프로세스와 공유가 가능하다는 점입니다.
예시) Mutex를 통한 데드락 방지 방법
Mutex mutex1 = new Mutex();
Mutex mutex2 = new Mutex();
void Method1()
{
mutex1.WaitOne();
try
{
// mutex1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
mutex2.WaitOne();
try
{
// mutex2에 대한 작업 수행
}
finally
{
mutex2.ReleaseMutex();
}
}
finally
{
mutex1.ReleaseMutex();
}
}
void Method2()
{
mutex1.WaitOne();
try
{
// mutex1에 대한 작업 수행
Thread.Sleep(1000); // 시간이 걸리는 작업을 시뮬레이션
mutex2.WaitOne();
try
{
// mutex2에 대한 작업 수행
}
finally
{
mutex2.ReleaseMutex();
}
}
finally
{
mutex1.ReleaseMutex();
}
}
위 코드는 두 개의 Mutex를 사용하여 두 개의 메서드에서 공유 자원에 대한 접근을 동기화합니다.
각 메서드는 두 개의 Mutex를 순서대로 획득하고, 작업이 완료되면 Mutex를 순서대로 해제합니다.
이 방식을 사용하면 데드락이 발생하지 않습니다.
하지만 주의할 점이 있습니다.
Mutex를 사용할 때 모든 WaitOne 호출에 대응하는 ReleaseMutex 호출이 있어야 한다는 것입니다.
그렇지 않으면 Mutex가 계속 잠기게 되어 다른 스레드에서 Mutex를 획득할 수 없게 됩니다.
이를 방지하기 위해 finally에서 mutex를 해제하는 것이 좋습니다.
| 마무리
데드락이 무엇인지 C#에서 데드락을 어떻게 해결할 수 있는지에 대해 알아봤습니다.
하지만 설명한 것들도 데드락을 완전히 방지할 수는 없습니다.
데드락을 피하는 가장 좋은 방법은 설계 단계부터 데드락이 발생할 수 있는 상황을 최대한 줄이는 것입니다.
또한 단순한 동기화 메커니즘을 사용하여 버그를 줄이는 게 가장 좋은 것 같습니다ㅎㅎ
틀린 점이나 질문이 있으시면 댓글로 남겨주세요!
다음 포스팅으로 찾아오겠습니다.
감사합니다 :)
'C#' 카테고리의 다른 글
C# Partial Class 이해 (0) | 2023.12.27 |
---|---|
C# 비동기 프로그래밍 : Event-based Asynchronous Pattern(EAP) 이해 (0) | 2023.11.27 |
C# 비동기 프로그래밍 : Thread와 Task의 차이점 (0) | 2023.11.21 |
C# 비동기 프로그래밍 : Task의 연속 작업 (Continuations) 이해 (0) | 2023.11.20 |
C# 비동기 프로그래밍 : async, await 이해 (0) | 2023.11.17 |