멀티 쓰레드
데드락
데드락의 발생 조건은 이전 시간에 알아본 상호배제 말고도 다양한 상황에서 다양한 조건으로 일어난다.
데드락 발생 조건의 다른 예시를 알아보자. 파란색이 화장실이라고 가정하고, 화장실에는 자물쇠가 2개가 잠겨 있는데, 이 때 직원 A, B가 화장실을 이용하기 위해 서로 경쟁을 한다고 가정해보자.
직원 A는 위의 자물쇠를 획득하고, 직원 B는 아래의 자물쇠를 획득했다. 하지만 자물쇠는 두 개를 동시에 가지고만 있어야만 열 수 있는데, 상황이 이렇게 되니 직원 A는 위 자물쇠를 획득한 상태에서 아래 자물쇠를 직원 B에게 달라고 요청을 하고, 직원 B는 아래 자물쇠를 획득한 상태에서 위 자물쇠를 직원 B에게 달라고 요청을 하는 상태가 된다. 즉 아래 이미지와 같이 되는 것이다.
따라서 이런 상황이 일어나는 근본적인 원인을 생각해보면 결국 자물쇠를 잠구는 순서가 맞지 않아서이다. 직원 A는 위부터, 직원 B는 아래서부터 잠그려고 했기 때문인 것이다. 따라서 이를 해결하기 위해서는 서로 규칙을 정하는 것이다. 자물쇠를 열 때에는 항상 위쪽 자물쇠를 먼저 열고, 그 자물쇠를 잠구는 사람이 아래 자물쇠도 잠구자는 규칙을 생성하면 된다.
그런데 왜 궂이 화장실은 하나인데, 자물쇠를 두 개나 써야할까? 예제로 보면 조금 이해하기 힘들지만 이는 코드를 이용해 확인해보면 직관적이다.
class SessionManager
{
static object _lock = new object();
public static void TestSession()
{
lock (_lock)
{
}
}
public static void Test()
{
lock (_lock)
{
UserManager.TestUser();
}
}
}
class UserManager
{
static object _lock = new object();
public static void Test()
{
lock (_lock)
{
SessionManager.TestSession();
}
}
public static void TestUser()
{
lock( _lock)
{
}
}
}
class Program
{
static void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
SessionManager.Test();
}
}
static void Thread_2()
{
for (int i = 0; i < 10000; 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);
}
}
위 소스코드를 살펴보면, 세션 매니저와 유저 매니저와 같은 두 개의 매니저를 가지고 있는 것을 볼 수 있다.
유저 매니저에서는 경우에 따라 유저가 잠수를 타고 있으면 유저를 방출하기 위해 세션 매니저에 접근해서 유저를 방출을 하고, 반대로 어떠한 이유에서 세션 매니저에서 유저 정보를 불러오기 위해 유저 매니저에 접근해 유저 정보를 호출 할 수 있다. 즉 서로 왔다 갔다하며 행동을 할 일이 빈번하게 생기는 것이다. 따라서 프로그램을 실행하면 당연하게도 데드락이 발생하게 된다. 이를 실행하여 디버깅을 통해 호출 스택을 확인해보면 각각의 스레드들이 서로에게 물려있는 모습을 확인할 수 있다.
그렇다면 데드락은 어떻게 해결해야 할까? 먼저 이전 시간에 배운 Monitor.TryEnter 메서드를 사용하여 요청을 시도 했을 때 일정 시간이 지나면 요청을 취소하는 방법이다. 이는 이론상으로는 매우 그럴싸하다. 그러나 락 획득을 실패를 했다는 것 자체가 이미 락 구조에 문제가 있다는 것을 내포하기 때문에 실패했을 경우를 위해 또 실패 처리를 로직을 짠다는 것 자체가 매우 비효율적이다. 일을 두번하는 셈 아닌가? 따라서 아예 크러시를 내는 상황을 예외처리 하는 것이 아니라, 크러시 자체를 수정하는 방법이 필요하다.
경우에 따라 로직이 더욱 복잡해져 클래스가 몇십, 몇백개가 늘어난다면 어떠한 곳에서 어떤 것이 데드락이 형성하는데 더욱 어려움이 생긴다. 슬프겠지만 데드락은 상황이 마주치면 그 때 가서 고치는 방법 밖에는 없다. 예방하는 것은 힘들진 몰라도 막상 겪게된다면 이유가 명확하게 찾아지게 된다.
또한 데드락이 더욱 무서운 이유는, 개발하면서는 생기지 않지만 라이브로 출시되고 유저가 몰릴 때 이러한 현상이 발생된다. QA 때에도 이런 문제가 없었는데 유저가 몰리면서 렉이 생길 때 이런 문제가 발생하는 것이다. 그렇다면 데드락은 예방하는 방법이 아예 없을까?
데드락의 해결
결론부터 얘기하면 데드락을 완전하게 방지하는 방법은 없다. 그렇지만 데드락을 해결하기 위한 다양한 꼼수들이 존재한다. 간단하게 생각한다면 아래와 같이 쓰레드를 잠시 멈추는 효과를 주어서 실행하면 자연스럽게 데드락은 형성되지 않는다.
Thread.Sleep(100);
그렇다고 해서 매번 이렇게 할 수는 없을 것이다. 따라서 프로젝트에 따라 lock을 클래스로 매핑을 하여 사용하는 방법이 있다. 따라서 매핑된 락을 이용하고, 해당 클래스에 있는 id를 통해 데드락을 회피하는 것이다.
class FastLock
{
public int id;
}
예를들어 세션 매니저의 경우 id를 1번으로 설정하고, 유저 매니저는 아이디를 2번으로 설정하는 방법을 이용하여 락이 호출되는 방법을 추적하는 것이다. 즉 패스트 락을 사용하여 id를 비교해 데드락을 회피하는 것이다. 그러나 이러한 방법은 클래스의 설계 구조가 미리 짜여져 있어야 하는 단점이 존재한다. 따라서 데드락은 완전한 방지는 없으며 패스트 락과 같은 추적을 통해 빠르게 해결할 수 있는 것이 최선의 방법이다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 4-2-10. 멀티쓰레드 프로그래밍 : SpinLock (0) | 2023.09.13 |
---|---|
Part 4-2-9. 멀티쓰레드 프로그래밍 : Lock 구현 이론 (0) | 2023.09.13 |
Part 4-2-7. 멀티쓰레드 프로그래밍 : Lock 기초 (0) | 2023.09.12 |
Part 4-2-6. 멀티쓰레드 프로그래밍 : interlocked (0) | 2023.09.12 |
Part 4-2-5. 멀티쓰레드 프로그래밍 : 메모리 배리어 (0) | 2023.09.11 |