Spinlock

1bio ㅣ 2025. 3. 30. 14:27

Spinlock이란?

SpinLock은 한 스레드가 락을 얻기 위해 반복해서 계속 확인(루프) 하는 방식의 락이다.

 

Spinlock을 활용하면 멀티 스레드를 사용할 때 다른 스레드의 접근 제한을 막을 수 있다.

아래는 While문을 활용한 스핀락 예시 코드다.

using System;
using System.Threading.Tasks;

// While 문을 활용한 스핀락 예시
namespace Server
{
    class SpinLock
    {
        volatile static bool _locked = false;

        public void Acquire()
        {
            while (_locked)
            {

            }

            _locked = true;
        }

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

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

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lockSpin.Acquire();
                _num++;
                _lockSpin.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lockSpin.Acquire();
                _num--;
                _lockSpin.Release();
            }
        }

        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(_num);
        }
    }
}

// 예상 출력 결과: 0
// 출력 결과: 랜덤한 값 출력

 

위의 코드를 실행하면 랜덤한 값이 출력된다.

왜 이런일이 발생할까?

 

만약, Acquire() 메소드에 있는 While 문을 두 개의 스레드가 동시에 통과할 때, CPU의 연산속도는 엄청 빠르기 때문에 각각의 스레드에서 lock을 가지고 있는 경우가 발생할 수 있다.

그렇게 되면 원자성(atomic)에 의해서 공유 변수를 가지고 있는 두 개의 스레드 사이에서 레이스 컨디션이 발생한다. 따라서, 원자성을 보호하기 위해서 여러 단계에 걸쳐서 계산하는 것을 방지해야 한다.

 

기존 While 문을 Interlocked.CompareExchange()로 대체하면 해결된다.

CompareExchange() 메소드는 어떤 스레드가 lock을 가지고 있으면 1을 반환하고 다른 스레드에서 접근하고 있지 않은 상태면 0을 반환해준다. 

lock 흭득 여부를 여러 단계를 거치지 않고 한 번에 확인할 수 있어서 원자성이 보장된다.

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

// Interlock을 활용한 예시
namespace Server
{
    class SpinLock
    {
        volatile static int _locked = 0;

        public void Acquire()
        {
            while (true)
            {
                int expected = 0; // 예상한 값
                int desired = 1; // 할당할 값

                // CAS(Compare-And-Swap)
                if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
                    break;

                // 위의 코드는 아래와 같다
                // if(_lock == 0)
                //      _lock = 1;
            }
        }

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

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

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lockSpin.Acquire();
                _num++;
                _lockSpin.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lockSpin.Acquire();
                _num--;
                _lockSpin.Release();
            }
        }

        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(_num);
        }
    }
}

// 출력 결과: 0

 

예상한 값과 할당할 값을 expected와 desired 변수로 네이밍 해주면 가독성이 더 좋아진다.

Acquire()이 끝나고 Release()를 사용하여 락을 해제해주면 다시 다른 스레드에서 접근이 가능해진다.

이 글은 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 참고하여 제작하였습니다.