저번 시간에 배운, 한 쪽이 자물쇠를 잠그고 풀지 않고 나가서 발생하는 데드락은 가장 기본적인 데드락이다.
이번에는 저번 시간보다 복잡한 데드락에 대해 배워 보자
다음과 같이 화장실에 2개의 자물쇠가 있다고 쳐보자.
화장실을 사용하려면 2개의 자물쇠를 모두 잠궈야 한다.
이때 그림과 같이, 한 사람은 자물쇠1을 잠그고 다른 사람은 자물쇠2를 잠궜다고 가정하자.
이러면 둘 모두 자물쇠1과 자물쇠2를 완전히 잠굴 수 없게 된다.
이 상황 역시, 한정된 자원을 여러 곳에서 사용하려고 할 때 모두 작업수행을 할 수 없이 대기 상태에 놓이는 상태 즉, 데드락(교착상태)이다.
위 상황을 코드로 구현해보자. 코드가 조금 복잡하므로 차근 차근 읽어 보자.
class SessionManager
{
static object _lock_session = new object();
public static void TestSession()
{
lock(_lock_session)
{
}
}
public static void Test()
{
lock (_lock_session)
{
UserManager.TestUser();
}
}
}
class UserManager
{
static object _lock_user = new object();
public static void TestUser()
{
lock(_lock_user)
{
}
}
public static void Test()
{
lock(_lock_user)
{
SessionManager.TestSession();
}
}
}
class Program
{
static int number = 0;
static object _obj = new object();
static void Thread_1()
{
for(int i=0; i<100; i++)
{
SessionManager.Test();
}
}
static void Thread_2()
{
for(int i=0; i<100; i++)
{
UserManager.Test();
}
}
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 에서 SessionManger.Test를 호출할 때 _lock_session을 사용해서 Lock을 걸고
이때 동시에 Thread_2 는 UserManger.Test를 호출해서 _lock_user를 사용해서 Lock을 건다.
Thread_1은 SessionManger.Test를 통해 UserManger.TestUser를 호출할 때 _lock_user로 Lock을 걸어야 한다.
근데 Thread_2에서 _lock_user를 사용하고 있다보니까 Lock을 걸 수 없고
마찬가지로, Thread_2 역시 UserManger.Test를 통해 SessionManager.TestSession을 호출할 때
_lock_session으로 Lock을 걸어야 하는데
Thread_1에서 이미 사용하고 있다보니 Lock을 걸 수 없다 보니
데드락에 빠지게 된다.
번외로, 데드락이 나면 예외처리로 고치기 보다는 차라리 크래쉬를 내는 것이 더 좋다.
Lock 구조에 문제기 있어 데드락이 발생한거기 때문에 그 상황에서 예외처리를 하는 것은 장기적인 관점에서 좋지 않다. 훗날 어떤 예외가 튀어 나올지 모르기 대문이다.
또 데드락이 무서운 이유는 조금의 시간차가 있으면 발생하지 않을 수 있기 때문이다.
위 코드에서
t1.Start();
Thread.Sleep(100);
t2.Start();
t1과 t2의 시작 시간을 다르게 해주면 코드가 정상적으로 작동하는 것을 볼 수 있다.
데드락은 이런 식으로 눈에 띄지 않을 수 있어 미리 예방하기가 어렵다.
따라서 데드락이 발생하면 그 때 수정하는 것이 좋은 방법이다.