[C# 서버] SpinLock

이정석·2023년 7월 31일
0

CSharpServer

목록 보기
2/13

SpinLock

SpinLock의 핵심은 Lock을 얻지 못했을 때 루프를 돌며 Lock을 얻는 시도를 반복하는 것이다. SpinLock을 구현할 때는 Lock을 얻는 함수를 호출한다면, 호출한 객체에 Lock을 얻었음을 보장해야 한다.

1. Main

Lock의 예제는 멀티쓰레드에서 사용한 정수형 자원의 증감 쓰레드를 사용하였고 해당 예제의 main함수는 다음과 같다. Thread1은 증가연산을 Thread2는 감소연산을 진행하고 두 쓰레드가 같은 횟수의 연산을 진행하기 때문에 정수형 자원의 최종값은 0이 되어야 한다.

    class Program
    {
        static int _num = 0;
        static SpinLock _lock = new SpinLock();

        static void Thread1()
        {
            for(int i=0;i<10000000;i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 10000000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread1);
            Task t2 = new Task(Thread2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1,t2);

            Console.WriteLine(_num);
        }
    }

각 쓰레드는 LockObject인 _lock을 통해 _num에 대한 Lock을 얻은 후 연산을 진행한다. SpinLock Class에서 동시성을 제어하는 구현에 따라 쓰레드의 Lock방식이 결정되는 구조이다.

2. SpinLock - 1

SpinLock의 구조는 Acquire()로 Lock을 하고 Release()로 Unlock하는 구조로 현재 Lock이 되어 있는지는 SpinLock Class의 _locked로 나타낸다. 간단하게 구현하면 아래와 같은 방식으로 구현할 수 있다.

    class SpinLock
    {
        volatile bool _locked = false;

        public void Acquire()
        {
            while(_locked)
            {

            }
            _locked = true;
        }

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

Lock을 얻는 Acquire()는 다른 쓰레드가 Lock을 얻었는지 나타내는 bool자료형인 _locked를 while문의 조건식으로 넣어 다른 쓰레드가 사용중이라면 루프를 도는 방식으로 구현하였다.

Release()는 단순히 _locked=false로 구현하였는데, Release()가 호출 될 때는 호출한 함수가 이미 상호 베타적인 Lock을 얻은 상태이기 때문에 문제가 되지 않는다.

놀랍게도 위의 코드로 main함수를 작동시키면 0이 나오지 않는다.

3. 왜 안될까?

_num의 최종값이 0이 아닌 이유를 알아내기 위해서 Acquire()의 구조를 먼저 살펴봐야할 필요가 있다. Acquire()의 구조를 나누면 크게 두 부분으로 나눌 수 있다.

  • 다른 쓰레드가 Lock을 얻었는지 확인하는 부분(Loop)
  • 다른 쓰레드가 사용하지 않고 있음을 확인하고 Lock을 얻는 부분(Assign)

위의 구조고 나눌 수 있는 이유는 당연히 코드상에 두 부분으로 나누어져 있기 때문이다. Loop에 해당하는 부분은 while(_locked)이고 Assign에 해당하는 부분은 _locked=true이다.

문제가 발생하는 상황은 아래그림과 같이 나타낼 수 있다. 쓰레드의 붉은 선은 현재 연산 진행중인 부분이다.

문제가 발생하는 상황에서 Thread1_locked==false임을 확인하고 Assign으로 내려가기 직전이고 Thread2Thread1_locked의 상태를 변경하기 전에 Loop문의 조건식을 지나 Loop를 벗어나기 직전이다.

두 쓰레드는 자신이 Lock을 정상적인 방법으로 얻었고 다른 쓰레드가 Lock을 얻지 못했다고 생각하게 될 것이고 동시에 _num에 쓰기 작업을 수행하게 될 것이다.

이 문제를 해결하기 위해 _locked에 읽기 작업과 쓰기 작업을 동시에 할 필요가 있다.

4. SpinLock - 2

읽기 작업과 쓰기 작업을 동시에 하기 위해 C#의 Interlocked를 사용할 수 있다.

InterlockedRaceCondition을 해결할 때 사용하는 방법 중 하나이다.

Acquire()에 Interlocked를 사용하기 위해

  1. _locked의 자료형을 정수형으로 바꾸어 준다.
  2. 다른 쓰레드가 사용하고 있음을 1로, 그렇지 않음을 0으로 나타낸다.
  3. Interlocked의 Swap관련 함수를 사용한다.

위 내용을 적용한 Acuire()는 아래와 같다.

    public void Acquire()
    {
        while (true)
        {
            int original = Interlocked.Exchange(ref _locked, 1);
            if (original == 0)
                break;
        }
    }

Exchange함수는 레퍼런스 변수를 지정한 값으로 바꾸는 함수이며 반환값은 Original Value이다. 이를 이용해 각 쓰레드는 _locked를 1로 바꾸고 original를 조사함으로 _locked를 정말로 자신이 바꿨다면 Loop를 빠져나오도록 한다.

original은 지역변수이므로 각 쓰레드의 스택영역에 존재한다. 즉, 다른 쓰레드에 영향을 미치지 않는 변수이다.

코드를 더 간소화 한다면 아래와 같이 나타낼 수 있다. Exchange함수가 아닌 CompareExchange를 사용하였고 비교하고자 하는 값을 매개변수로 넣어 _locked의 값이 0이라면 1로 바꾸고 그렇지 않다면 바꾸지 않는다. CompareExchange의 반환값은 Exchange와 같은 Original Value이다.

    public void Acquire()
    {
        while (true)
        {
            if (Interlocked.CompareExchange(ref _locked, 1, 0) == 0)
                break;
        }
    }

Interlocked는 정수형을 대상으로 하는 클래스로 자세한 내용은 공식문서를 참조하자!


System.Threading.SpinLock

C#의 System.Threading 산하에는 SpinLock이 구현되어 있는데 이를 이용해 Lock을 구현할 수 있다. Threading.SpinLock의 기본적인 인터페이스는 EnterExit이 있는데 각각 위 예제의 Acquire, Release에 해당한다.

  • Enter(ref bool lockTaken): Lock을 얻는 함수 만약 다른 쓰레드가 Lock을 사용하고 있다면 얻을 때까지 Enter를 호출한 쓰레드는 Block된다. 매개변수로는 Enter함수의 안전성을 위해 사용되는 lockTaken이 있다. Lock을 정상적으로 얻었다면 lockTakentrue가 된다.
  • Exit(): Lock을 해제하는 함수로 Enter로 Lock을 얻은 후 Exit로 얻은 Lock을 해제해주어야 한다.
profile
게임 개발자가 되고 싶은 한 소?년

0개의 댓글