📄 스핀락

스핀락은 잠금을 획득하려는 스레드가 특정 코드가 현재 사용 가능한지 반복적으로 확인하면서 단순히 루프('스핀')에서 기다리는 것입니다. 스레드는 활성 상태를 유지하지만 계속 락에 대한 검사를 수행하는 형태를 띱니다.

 

장점:

spinning 중이고 스레드가 휴지 상태가 아니기 때문에 콘텍스트 전환이 필요하지 않습니다.

크리티컬 섹션(스레드가 동시에 접근해서는 안 되는 공유 자원을 접근하는 코드)이 작으면 도움이 됩니다.

 

단점:

Spinlock은 spinning이 필요합니다.
락 상태에서 사용할 수 없으면 액세스 가능한지 반복적으로 확인합니다.(CPU를 낭비)

 

https://www.baeldung.com/cs/spinlock-vs-semaphore

 

📑 SpinLock 이론

namespace ServerCore
{
    class SpinLock
    {
        private volatile bool mLocked = false;

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

            mLocked = true;
        }

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

    class Program
    {
        static int number = 0;
        static SpinLock spinLock = new SpinLock();

        static void Thread1()
        {
            for(int i = 0; i < 100000; ++i)
            {
                spinLock.Acquire();
                number++;
                spinLock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 100000; ++i)
            {
                spinLock.Acquire();
                number--;
                spinLock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task task1 = new Task(Thread1);
            Task task2 = new Task(Thread2);

            task1.Start();
            task2.Start();

            Task.WaitAll(task1, task2);

            Console.WriteLine(number);
        }
    }
}
  • 스핀락을 간단히 구현한 형태입니다.
  • 단순하게 접근하여 bool변수가 true(locked)이면, while문을 계속 돌며 locked가 false가 될 때까지 기다립니다.
  • locked가 false가 되면 locked를 true로 바꾸고, 자신의 코드를 수행하고 다시 false로 바꾸는 것을 반복하는 구조입니다.

 

  • 예상과 달리 0이 나오지 않고 예측 불가능한 값이 나왔습니다.
  • mLocked가 false인지 확인하고, true로 바꾸는 과정에서 원자성이 보장되지 않은 코드이기에 발생하는 문제입니다.
  • 원자성을 보장하는 기능을 포함하여 스핀락을 구현해야합니다.

 

📑 SpinLock 1

namespace ServerCore
{
    class SpinLock
    {
        private volatile int mLocked = 0; //0: Unlocked, 1: Locked

        public void Acquire()
        {
            while(true)
            {
                //리턴값은 mLocked을 넣기 전의 값(prev value)이고, 1로 변경하고자 함
                //리턴값이 이미 1이었다면, 다른 스레드에서 이미 잠근상태에서 사용중이라는 뜻으로 획득 불가
                int original = Interlocked.Exchange(ref mLocked, 1);

                //만약 리턴값이 0이라면, mLocked는 0, 아직 잠기지 않은 상태이므로 획득 가능
                if (original == 0) { break; }
            }
        }

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

    class Program
    {
        static int number = 0;
        static SpinLock spinLock = new SpinLock();

        static void Thread1()
        {
            for(int i = 0; i < 1000000; ++i)
            {
                spinLock.Acquire();
                number++;
                spinLock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 1000000; ++i)
            {
                spinLock.Acquire();
                number--;
                spinLock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task task1 = new Task(Thread1);
            Task task2 = new Task(Thread2);

            task1.Start();
            task2.Start();

            Task.WaitAll(task1, task2);

            //예상되는 출력은 0
            Console.WriteLine(number);
        }
    }
}
  • Interlocked 기능을 사용한 방법입니다.

 

정상적으로 0이 나온것을 볼 수 있다.

 

int original = Interlocked.Exchange(ref mLocked, 1);
  • Interlocked.Exchange 함수를 사용하여 현재 mLocked의 값이 두 번째 매개변수인 {1}로 바뀌며, 리턴값은 바꾸기 이전의 값입니다.
  • 리턴값은 mLocked을 넣기 전의 값으로 나온 상태로, 만약 값이 0이라면 아직 잠기지 않은 상태로 사용이 가능합니다.
  • 리턴값이 1이라면 다른 스레드에서 이미 잠근상태에서 사용 중입니다.

 

📑 SpinLock 2

namespace ServerCore
{
    class SpinLock
    {
        private volatile int mLocked = 0; //0: Unlocked, 1: Locked

        public void Acquire()
        {
            while(true)
            {
                /*
                매개 변수
                            location1   comparand 값과 비교되어 value로 바뀔 수 있는 값을 가진 대상입니다.
                            value       비교 결과가 같은 경우 대상 값을 바꿀 값입니다.
                            comparand   location1의 값과 비교할 대상입니다.

                반환
                            location1의 원래 값입니다.
                */
                //리턴값이 0일경우, 언락 상태기에 1로 바꿔 다른스레드에서 사용할 수 없게 한다
                //리턴값이 1인경우, 이미 언락 상태이기에 값은 변하지 않음

                int expected = 0; //잠겨져있지 않을때 나올 값
                int desired = 1; //잠궜을때 설정할 값

                if (Interlocked.CompareExchange(ref mLocked, desired, expected) == expected) { break; }
            }
        }

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

    class Program
    {
        static int number = 0;
        static SpinLock spinLock = new SpinLock();

        static void Thread1()
        {
            for(int i = 0; i < 1000000; ++i)
            {
                spinLock.Acquire();
                number++;
                spinLock.Release();
            }
        }

        static void Thread2()
        {
            for (int i = 0; i < 1000000; ++i)
            {
                spinLock.Acquire();
                number--;
                spinLock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task task1 = new Task(Thread1);
            Task task2 = new Task(Thread2);

            task1.Start();
            task2.Start();

            Task.WaitAll(task1, task2);

            //예상되는 출력은 0
            Console.WriteLine(number);
        }
    }
}
  • 위 방법과 같은 원리로 동작하는 다른 함수입니다.

 

정상적으로 값이 나오는걸 볼 수 있다.

  • 리턴값은 동일하게 mLocked의 원래 값입니다.
  • desired는 mLocked와 expected를 비교하여 값이 같으면 mLocked를 바꿀 값입니다.
  • expected는 mLocked와 비교할 값입니다.
  • 리턴값이 0이라면, 함수를 호출할 때 잠기지 않은 상태이고,  expected와 같기 때문에 mLocked는 1로 바뀌었습니다.
  • 다른 스레드가 같은 함수를 호출하면 이미 1로 바뀌어있기 때문에 잠긴 상태가 되어 다음 단계로 넘어갈 수 없습니다.

'server > socket server' 카테고리의 다른 글

[C# 서버] AutoResetEvent  (1) 2023.01.06
[C# 서버] Context Switching  (0) 2023.01.04
[C# 서버] 원자성, Interlocked, Monitor, lock  (0) 2023.01.02
[C# 서버] 메모리 배리어  (0) 2023.01.02
[C# 서버] volatile  (1) 2022.12.30
bonnate