Q1. DeadLock이 발생하는 경우의 예를 설명해 보세요.
Q2. DeadLock이 발생할 확률을 줄이는 방법을 두 가지 설명해 보세요.
오른쪽 직원은 하염없이 기다리는 상태
왼쪽 직원 나오면 오른쪽 직원이 똑같이 들어가서 문 잡그고 사용하는 상태
데드락은 한 직원이 들어가서 영영 풀어주지 않으면 다른 직원 영영 못들어가는 상황 되는 거
Lock이라는 키워드로 자동으로 Lock을 해제하는 거 까지 알아봤다. 데드락 중에서 가장 기초적이고 간단한 부분에 속한다.
일반적인 상황에서는 좀 더 고차원적인 상황에서 발생한다.
둘 다 잠궈야 들어갈 수 잇는 상황
각자 하나씩 잠궜어.
나머지 하나의 자물쇠도 획득하려고 노력한다.
영원히 서로 두개를 동시에 획득하는 경우는 없을거야. 서로 뺑뺑이로 물고 물리는 상황이 되는 거 .
이런 상황 발생하는 근본적인 이유는 자물쇠를 잠그는 순서가 안맞기 때문이다.
고치는 방법은 서로 규약을 하나 정하는 거. 무조건 이쪽 자물쇠 부터 먼저 잠그고, 그 자물쇠를 잠근 사람이 2번 자물쇠도 잠그자 라고 어느정도 규칙을 정해야 한다.
운좋게 왼쪽 애가 자물쇠1을 먼저 획득하면 게임이 끝나는 거 .
두번째 자물쇠도 잠궈주면 화장실에 들어갈 수 있게 된 거.
왜 굳이 화장실 하나에 자물쇠 2개 사용하는지 코드에서 보면 직관적이야.
MMORPG에서 락이 클래스 안에 들어가 있는 경우가 많다.
class SessionManager
{
static object _lock = new object();
}
class UserManager
{
static object _lock = new object();
}
서로 접근할 일이 발생
namespace ServerCore
{
class SessionManager
{
static object _lock = new object();
void TestSession()
{
lock(_lock)
{
}
}
}
class UserManager
{
static object _lock = new object();
void Test()
{
lock(_lock)
{
TestSession();
}
}
}
UserManager에서 자기 락을 잡은 상태에서 TestSession을 실행하게 된면
UserManger에서 락을 먼저 잡고, SessionManager에서 TestSession을 실행하는 셈이다.
반대로 보면 SessionManager에서도 뭔가를 할건데,
class SessionManager
{
static object _lock = new object();
void TestSession()
{
lock(_lock)
{
}
}
void Test()
{
lock(_lock)
{
TestUser();
}
}
}
class UserManager
{
static object _lock = new object();
void Test()
{
lock(_lock)
{
TestSession();
}
}
void TestUser()
{
lock(_lock)
{
}
}
}
Test에서 자기거를 먼저 lock을 건 다음에 TestUser 라는 상대방 것을 호출을 하면서
서로 뒤엉켜서 서로 lock을 획득하려는 시도를 할거야.
그 상황에서 데드락 일어나는 걸 테스트 해보자.
namespace ServerCore
{
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)
{
}
}
}
이렇게 static과 public을 세팅하고 각각의 스레드에서 하나는 SessionManager에서 Test를 호출하고, 하나는 UserManager의 Test를 호출하면 꼬이는 현상이 발생할 것이다.
internal class Program
{
static int number = 0;
static object _obj = new object();
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);
Console.WriteLine(number);
}
}
이렇게 했을 때 Console.WriteLine(number); 여기에 중단점 찍고 실행했을 때 여기까지 오면 데드락 안걸리고 무사히 끝냈다는 걸테고, 그게 아니라 영영 빠져나오지 못한다면 데드락 상황이라고 생각할 수 있어. 실행을 해보면
빠져나오지 않는 거 볼 수 있다.
모두 중단을 눌러보면
여기에서 멈춰있는 거 볼 수 있다.
각각의 작업자 스레드가 뭘 하고 있는지 살펴보면
TestUser라는 곳에 물려 있는 거 볼 수 있고,
콜스택을 보면 SessionManager의 Test에서 락을 잡은 상태에서 UserManager의 UserManager.TestUser를 호출해 이쪽 락을 잡으려고 하는 거였고,
또 다른 스레드에서는
UserManager의 Test에서 락을 잡고 SessionManager.TestSession();를 호출해서 SessionManager에서 락을 잡으려고 하는 곳에서 멈춰 있다.
이렇게 서로 사이클이 돌기 때문에 발생한 문제라고 볼 수 있다.
해결 방법을 생각해보자.
락을 했을 때 어느정도 해보다가 안되면 포기하는 방법이 있을 거야. Monitor.TryEnter()라는 함수가 있기는 한데 실패를 했다는 거 자체가 애초에 Lock 구조에 문제가 있다는 게 될거야.
실패했을 때 가정해서 코드 이중으로 짜는게 현명해 보이지 않아.
try, catch랑 비슷해. 이걸 사용하기 보다 그냥 크래시 내고 고치는게 현명하다고 생각해.
이처럼 데드락도 일어나면 고치는게 훨씬 낫다.
경우에 따라 복잡한 구조가 될 수 있어. 클래스가 몇십개가 되면 서로 어떤 식으로 락을 획득하는지 미리 알고 짜는 프로그래머는 없을 거야. 나중에 가면 기반 코드는 이전 사람이 만든 경우가 많기 때문에 더욱더 락이 어떤 순서로 호출되는지를 알기가 힘들다는 얘기가 된다.
대부분은 데드락 일어나면 그 상황을 보고 고치는 경우가 많다.
예방이 힘들지만 발생하면 콜스텍 추적하면 이유는 보인다.
구조가 많이 꼬이면 어렵지만 일반적인 경우는 크래쉬 내고 고치는게 현명하다.
데드락이 끔찍한게 개발 단계에서는 일어나지 않다가 라이브로 가서 유저들이 몰릴 때 터지는 경우가 많다.
대부분은 UserManager와 SessionManager가 동시에 일어나지 않고 띄엄띄엄 일어날거야.
internal 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();
Thread.Sleep(100);
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(number);
}
}
Thread.Sleep(100); 으로 텀을 주고 100번만 반복하게 했다.
이렇게 수정을 하고 다시 f5를 눌러 보면
이제는 데드락이 일어나지 않고 무사히 통과한 것을 볼 수 있다.
동시에 실행하면 데드락 발생하지만 이런식으로 텀을 두고 실행하니 아무런 일 없이 잘 되는 걸 볼 수 있다.
크래쉬 나면 고친다는게 책임감 없어 보인다면 여러 꼼수가 더 있긴 하다.
어떤 프로젝트에서는 _lock을 생으로 안쓰고 어떤 클래스로 매핑을 한번 한다.
namespace ServerCore
{
**class FastLock
{
public int id;
}**
class SessionManager
{
**FastLock l;**
static object _lock = new object();
public static void TestSession()
{
lock(_lock)
{
}
}
public static void Test()
{
lock(_lock)
{
UserManager.TestUser();
}
}
}
class UserManager
{
**FastLock l;**
static object _lock = new object();
public static void Test()
{
lock(_lock)
{
SessionManager.TestSession();
}
}
public static void TestUser()
{
lock(_lock)
{
}
}
}
만약 SessionManager의 FastLock l을 잡고 있는 상태에서 다른 애를 실행하는데 그게 현재 잡고 있는 id보다 높으면 문제가 있는 거라고 크래쉬를 내는 그런 경우도 있었어. 사실 그건 코드를 짤 때 클래스의 구조를 다 짜서 어떤 애를 호출 할 수 있을 것이다라는 걸 미리 짜야 가능한 부분이야. 사실 실용적인지는 모르겠어. 그렇다고 데드락이 완전 없어진다 볼 수는 없다.
그리고 id를 부여해서 추적을 하는 방법들. id 호출 순서를 추적했다가 그래프를 만들어서 그래프에 사이클이 있는지를 판별하면 데드락 상황에 있다는 걸 발견할 수 있다. 그런 상황 모두 데드락을 완벽 봉쇄하겠다는 방식은 아냐.
사실 Run을 해보니 문제가 될 소지가 있었는데 지금처럼 타이밍이 어긋나서 크래시가 안났다는 것만 판별할 수 있으면 게임이 라이브로 나가기 전에 발견할 수 있으면 의미가 있을 거야. id를 만들어서 추적하는 건 그런 의미에서 하는 거. 라이브 나가기 전에 지나치는 걸 방지하겠다는 의미로 받아들이면 된다.
이렇게 해도 대부분의 경우는 데드락을 완전히 막을 수 없다. 생각보다 까다롭기에 실제로 발생하면 고치는게 더 쉽다. 가 결론
'Server programming' 카테고리의 다른 글
01_10_멀티쓰레드_SpinLock (0) | 2023.03.15 |
---|---|
01_09_멀티쓰레드_Lock 구현 이론 (0) | 2023.03.14 |
01_07_멀티쓰레드_Lock 기초 (0) | 2023.03.13 |
01_06_멀티쓰레드_Interlocked (0) | 2023.03.13 |
01_05_멀티쓰레드_메모리 베리어 (0) | 2023.03.13 |
댓글