본문 바로가기
게임 서버/Thread

[c#/유니티] DeadLock과 SpinLock에 대해

by 얘리밍 2022. 12. 8.
320x100
728x90

DeadLock에 대해 알아보자

 

📌  데드락(DeadLock)이란?

              두 개 이상의 프로세스가 서로의 작업이 끝나기만을 기다림. 둘 다 대기상태에 이르러 영원히 끝나지 않은 상황이다.

              교착 상태라고도 말한다. 

 

                        👉 한정된 자원을 얻기 위해 서로 경쟁하기 때문에 발생한다. 

 

 

 

       🔎  DeadLock의 4가지 필요 조건

               

                아래 4가지 조건이 모두 만족되는 경우, 데드락이 발생하라 가능성이 있음

                하나라도 만족하지 않으면 절대로 발생하지 않는다. -> 하나라도 해결되면, 데드락 문제를 풀 수 있다. 

 

 

                  •   상호 배제(Mutual Exclusion)

                            하나의 자원은 한 번에 한 프로세스만 사용할 수 있다. 임계 영역(Critical Section)을 뜻한다. 

 

                 점유와 대기(Hold and wait)

                            어떤 프로세스가 하나 이상의 자원을 점유하고 있으면서 다른 프로세스가 가지고 있는 자원을 기다림 

 

                 • 비선점(No Preemption)

                            프로세스가 작업을 마친 후, 자원을 자발적으로 반환할 때 까지 기다림(강제로 뺏지 않는다)

                            다른 프로세스 혹은 스레드가 획득한 자원이 반환될 때 까지 기다림을 뜻한다. 

 

                 • 환형 대기(Circular Wait)

                            Hold and Wait 관계의 프로세스들이 서로를 기다림 

                            T1(A -> B), T2(B -> C), T3(C -> D), T4(D -> A) 와 같이 서로가 물려있는 상태를 뜻한다. 

 

 

 

 

      💥  해결 방법

 

                 1. 상호배제 제거 

                         자원 공유를 가능하게 한다 -> 거의 불가능 

                         싱글 스레드거나 공유 데이터가 없으면 상호배제 영역이 제거 된다.

 

                2. 점유대기 제거

                         lock 거는 코드를 제거한다. 프로세스 실행 전 모든 자원을 할당한다. 

 

                3. 비선점 제거 

                        이미 걸려있는 lock을 임의로 해제한다. 

                        자원 점유 중인 프로세스가 다른 자원을 요구할 때 가진 자원을 보내도록 한다. 

 

                4. 순환대기 제거 

                        lock에 방향성을 넣어, 하나의 방향으로만 lock을 걸도록 처리한다. 

 

 

 

 

           코드를 보며 이해해 보자. 

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

    public static void TestSession()
    {
        lock (_lock)
        {
            // SessionManager의 작업을 여기서 수행
        }
    }

    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)
        {
            // UserManager의 작업을 여기서 수행
        }
    }
}

 

 

       SessionManagerUserManager 클래스의 Test 메소드는 각각의 고유한 TestSession과 TestUser를 호출하고 있다. 

       SessionManager를 먼저 보면, Test의 크리티컬 섹션에서 UserManager의 TestUser를 실행하려고 호출했지만,

       이미 TestUser 메소드lock에 걸려 선점된 상태이다.  SessionManager는 선점된 상태를 그저 바라볼 뿐이다.

 

 

       UserManager에서의 Test 메소드에서 또한 SessionManager의 TestSession 메소드를 호출했다.

       하지만 해당 메소드는 이미 선점되어서 lock이 걸린 형태이다. UserManager도 이를 바라 볼 뿐이다.

 

       이렇게 서로의 작업이 끝나기만을 기다리는 상태가 교착상태(DeadLock)이다. 

 

        쉽게 말해서, A는 나이프를 선점한 상태(갖고 있는)에서 B에게 포크를 달라 하고 있고, B는 포크를 선점한 상태(갖고 있는)          에서 A에게 나이프를 달라고 하며 서로 대기하는 상태인 것이다. 

 

 

 

 

 

 

 

 

📌   SpinLock 이란?

             lock은 상호 배타적 특성을 구현하는 방법이라 할 수 있다. 

            스핀락은 락의 한 종류로 루프를 돌면서 계속 점유를 시도하는 방법을 채택하는 락이다. 

            스레드 동기화(Thread-safe)를 구현하기 위한 .NET 클래스 중 하나이다. 

           

            장점 : 문맥교환(Context Switching)이 발생하지 않아 CPU 부하를 줄일 수 있다.

                 👉 하지만 한 스레드가 Lock을 오랫동안 소유하고 있다면, 다른 스레드들은 계속해서 무한루프만 돌 것이다 ❗

                       따라서 임계 영역이 짧거나, 빨리 처리가 가능한 경우에 사용하는 것이 용이하다. 

 

 

* 상호 배타적 : 어느 한 사건이 일어났을 때, 다른 사건이 발생할 수 없음. 즉, 동시에 일어날 수 없는 사건 

스레드 동기화 : 각 스레드들이 순차적으로 혹은 제한적으로 접근하도록 하는 것.

 

 

 

 

 

 

             💥  SpinLock 구현 

 

                      1. lock을 다른 스레드가 선점하고 있으면, lock이 풀릴 때까지 무한 루프를 돈다(spin).

                      2. lock을 선점하고 있는 스레드가 없으면, 해당 lock을 얻고 lock 상태를 변경한다(소유중)

 

class SpinLock
{
    volatile bool _locked = false;

    public void Acquire()
    {
        while (_locked)
        {
            // 잠김이 풀리기를 기다린다
        }

        _locked = true;
    }

    public void Release()
    {
        _locked = false;
    }
}

 

하지만 다음과 같은 코드는 아래와 같이 제대로 된 값을 출력하지 않는 것을 볼 수 있다. 

 

잘못된 결과값 출력

 

              while문을 도는 상황에서 다른 스레스가 접근 순서를 달리할 수 있어 경쟁 상태가 발생하기 때문이다. 

 

*경쟁 상태(Race Condition) : 프로세스가 어떤 순서로 데이터에 접근하느냐에 따라 결과값이 달라질 수 있는 상황.

둘 이상의 입력이나 조작이 동시에 일어나 의도하지 않은 결과를 가져오는 경우.

 

 

 

 

 

 

 

 

          🔎  해결 방법

                     Interlocked 클래스의 Exchange() 혹은 CompareExchange()  메소드를 통해 이를 해결할 수 있다. 

 

                     

 

 

                    📌  Interlocked.Exchange(ref location, value) 메소드 

 

                              location 변수를 value 값으로 초기화 하고, 초기화 되기 전의 값(원래의 값)을 리턴한다.

 

 

                           1. _locked를 1로 계속 초기화 하며 초기화 이전 값을 original에 할당하며 이 값을 계속 확인한다.

                           2. original이 1이면 lock 이 걸려있다는 의미로 계속 반복문을 돌면서 확인하고, original이 0이면 lock이 풀                                   려 있다는 의미로 반복문을 나와 진입한다. 

 

class SpinLock
{
    volatile int _locked = 0;
    public void Acquire()
    {
        while (true)
        {
            int original = Interlocked.Exchange(ref _locked, 1); //스택에 들어 있는 값 
            if (original == 0)
                break;
        }

    }
    public void Release()
    {
        _locked = 0;
    }
}

다음과 같이 정상적으로 출력되는 것을 볼 수 있다. 

 

 

 

 

 

 

 

                   📌  Interlocked.CompareExchange(ref location, value, comparand) 메소드

 

                                location 값이 comparand와 같으면 location을 value로 초기화하고, location의 원래 값을 리턴한다. 

 

                                 👉이 방법은 CAS(Comapre-And-Swap)에 해당하며 원자성을 가지며 update된 값을 가짐을 보장한다. 

 

*CAS(Comapre-And-Swap) : 비교한 후 값을 바꾸어 주는 것 

 

                                 

 

                        1. expected는 비교하는 값, desire은 새로 교체할 값에 해당한다.

                        2. _locked와 expected가 동일하다면 _locked값을 desire 값으로 초기화 하고, _locked의 원래 값을 반환한다.

                               

                                👉 반복문을 돌면서 계속 확인하다가, expected와 _lock의 값이 같다면 _lock 값을 desire로 바꾸어주고                                        반복문을 종료하여 lock을 획득한다 라는 의미이다. 

class SpinLock
{
    volatile int _locked = 0;
    public void Acquire()
    {

        while (true)
        {
            int expected = 0;
            int desire = 1;
            //int original = Interlocked.CompareExchange(ref _locked, desire, expected);
            if (Interlocked.CompareExchange(ref _locked, desire, expected) == expected)
                break;
        }
    }

      public void Release()
      {
          _locked = 0;
      }
 }

         

            이 또한 결과값이 올바르게 나옴을 확인할 수 있다. 

          CAS를 보장하는 Interlocked.CompareExchange 방식을 주로 활용하도록 하자.

 

 

 

 

 

 

출처 및 참고 : https://itwiki.kr/w/%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9C

 https://www.inflearn.com/course/유니티-mmorpg-개발-part4

728x90
반응형