Skip to content

Latest commit

 

History

History
378 lines (275 loc) · 9.95 KB

File metadata and controls

378 lines (275 loc) · 9.95 KB

image

공용 화장실을 직원 2명이 동시에 사용하려고 하는데

먼저 들어간 사람이 자물쇠로 문을 잠구면 다른 직원이 접근할 수 없는 상태가 됨

image

단 다른 문을 잠근 상태에서 들어갔다가 자물쇠를 다시 안풀고 나가는 상태가 DeadLock

이 경우가 DeadLock의 가장 기초적인 문제

일반적으로 방생하는 Deadlock은 좀더 고차원적인 상황

image

자물쇠가 2개가 있고 이 자물쇠를 2개다 잠궈야지만 들어갈 수 있다고 가정

image

각 직원이 자물쇠를 각각 하나씩 잠금

image

그 다음에 2개의 자물쇠를 획득해야(잠궈야) 들어갈 수 있기 때문에 나머지 자물쇠를 획득하기 위해 노력하는 중

문제는 각자 직원이 한쪽 짝을 서로 가지고 있으니깐 영원히 동시에 2개의 자물쇠를 획득하지 못하는 상황이 발생 ⇒ 이런 경우가 생각보다 자주 일어나게 됨

왜 이런 일이 일어났을까?

자물쇠를 잠구는 순서가 안맞았다.

왼쪽 직원은 자물쇠1을 잠구고 자물쇠 2를 잠구려고 했는데

오른쪽 직원은 자물쇠 2를 잠구고 자물쇠 1을 잠구려고 했기 때문에

이런 식으로 cycle이 발생함

이를 해결하는 방법은

자물쇠를 잠구는 순서를 정하는 것이다.

즉, 무조건 자물쇠 1번부터 잠구고 1번을 잠군 사람이 자물쇠 2번을 잠굴 수 있도록 규칙을 정하는 것이다.

image

위와 같은 규칙하에서

왼쪽 직원이 자물쇠 1을 획득했고

image

자물쇠 2번을 획득했다.

왜 굳이 화장실은 하나인데 자물쇠는 2개일까?

코드에서는 직관적으로 확인이 가능

[예제]

class SessionManager
{
    static object _lock = new object();

    public static void TestSession()
    {
        // 자신의 lock을 잡음
        lock (_lock)
        {
            
        }
    }

    public static void Test()
    {
        // 내 lock을 걸고 상대방의 lock을 획득하려고 하는 경우
        // 왜냐하면 lock 내부에 Monitor.Enter가 구현이 되어 있기 때문에 
        lock (_lock)
        {
            UserManager.TestUser();
        }
    }
}   

class UserManager 
{
    static object _lock = new object();

    public static void Test()
    {
        // 내 lock을 걸고 상대방의 lock을 획득하려고 하는 경우
        // 왜냐하면 lock 내부에 Monitor.Enter가 구현이 되어 있기 때문에 
        lock(_lock)
        {
            SessionManager.TestSession();
        }
    }
    public static void TestUser()
    {
        lock(_lock)
        {

        }
    }
}

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);

        System.Console.WriteLine(number);
    }
}

큰 규모의 MMO서버를 다룰 때는lock object를 각 class에서 보관을 한다.

예을들어 위 Manager Class들이 각자의 lock을 가지고 있고,

위 두 manager들 끼리 서로 접근을 해야하는 경우가 있다고 가정해보자.

위와 같은 경우가 그림에서와 같이 서로 락을 획득하려고 달려드는 상황이다.

Thread_1과 Thread_2가 각각의 Manager에서 Test()를 실행을 하게 되는데

해당 Test()에서 각각 TestSession()과 TestUser()를 실행하고 있고

각 메서드에서도 마찬가지로 lock을 잡고 있기 때문에 2개의 lock을 서로 획득하려고 달려드는 그림이다.

이러한 상황도 역시 DeadLock이 발생하는 상황이다.

[해결방법]

class SessionManager
{
    static object _lock = new object();

    public static void TestSession()
    {
        // 자신의 lock을 잡음
        lock (_lock)
        {
            
        }
    }

    public static void Test()
    {
        lock (_lock)
        {
            UserManager.TestUser();
        }
    }
}   

class UserManager 
{
    static object _lock = new object();
    public static void Test()
    {
        Monitor.TryEnter(null, TimeSpan.MaxValue);

        lock(_lock)
        {
            SessionManager.TestSession();
        }
    }

    public static void TestUser()
    {
        lock(_lock)
        {

        }
    }
}

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);

        System.Console.WriteLine(number);
    }
}

안타깝게도 가장 좋은 해결 방법은 없다고 가정한다.

우선 UserManager 에서 구현한 TryEnter를 통해 일정 시간이 지나면 깔끔하게 포기하는 방법을 사용할 수도 있다.

하지만 이것도 애초에 TryEnter가 실패했다는 것은 lock 구조에 문제가 있다는 이야기다.

따라서 Deadlock 일어나면 그때가서 고치는 방법이 가장 좋다.

나중에 수십개의 클래스에서 작업을 할 때 서로 어떤 식으로 lock을 획득하는지 처음부터 다 알고

짜는 프로그래머는 없을 것이다.

게다가 이런 기반 코드는 이전 사람들이 짠 경우가 많기 때문에

lock이 어떤 순서로 호출되는지 알기가 정말 힘들다.

그래서 데드락이 일어나면 그 상황을 보고 고치는 경우가 많음

왜냐하면 처음에 예방하기가 힘들어서 그렇지 막상 발생하면 콜스택을 추적해서 고치기는 쉽다.

일반적인 경우에 크래싱을 내서 수정을 하는 방법을 많이들 사용한다.

무튼 이런 데드락 상황이 끔직한 이유는 개발 단계에는 잘 일어나지 않다가

라이브 상태에서 유저들이 몰릴 때 터지는 경우가 많음

하지만 위와 같이 Crashing 발생하고 난 다음에 수정을 해야한다고 하면 매우 무책임하게 느껴질 수도 있기 때문에 몇가지 꼼수가 있는데 그 중 한가지는 id를 부여하는 방법이 있다.

class FastLock
{
    public int id;
		object _lock = new object();
}

각 클래스에서 FastLock이라는 클래스를 가지고 있고 모두 FastLock을 통해 lock을 관리한다고 가정한다.

그래서 각 Manager 클래스에서 FastLock 객체를 생성한 다음에 서로 락이 어떤 식으로 호출되는지를 확인 방법을 사용하는 것도 추후 누가 어떤 순서로 lock을 획득했는지를 확인할 수도 있다.

하지만 이 또한 근본적인 해결 방법은 아니다.

[개발 단계에서 DeadLock을 발견하기 어려운 이유]

using System;
using System.Threading.Tasks;
using System.Threading;

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()
        {
            Monitor.TryEnter(null, TimeSpan.MaxValue);
            lock(_lock)
            {
                SessionManager.TestSession();
            }
        }
        public static void TestUser()
        {
            lock(_lock)
            {

            }
        }
    }

    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);

            System.Console.WriteLine(number);
        }
    }
}

보통의 경우 Thread.Sleep(100); 을 사용해서

0.1초정도 여유를 준 상태에서 다음 쓰레드를 실행시키 때문에

위의 DeadLock 상태가 잘 발생하지 않는다.

왜냐하면 Deadlock이 발생한 이유는 동시에 쓰레드를 실행시켰기 때문이다.