CS/운영체제

C#으로 구현한 스핀락

alpacadabra 2022. 11. 22. 00:15

스핀락은 그냥 공회전이라고 생각하면 된다.

특별히 하는건 없지만, 굳이 자원을 소모해가며 스레드의 주도권을 유지하는 것이다.

물론 그렇게 하는 데에는 이유가 있다. 스핀락으로 인한 낭비와 컨텍스트 스위치로 인한 낭비를 비교했을 때 전자가 더 저렴한 경우가 존재하기 때문이다.

예를 들어, 임계 구역에서의 작업이 단시간에 끝나는 경우 모니터락보다 스핀락이 더 효율적일 수 있다.

 

C#에는 이미 내장된 SpinLock 클래스가 존재하지만, 뭐든지 몸소 체득하는 편이 빠르므로 스핀락을 직접 구현해보기로 하였다.

 


 

스핀락의 대기는 무한루프로 이루어진다. 따라서 코드를 아래와 같이 작성할 수 있다.

 

class MySpinLock
{
    int flag = 0; //0 = unlocked, 1 = locked

    public void Enter()
    {
        while (flag == 1); //while locked, do nothing
        flag = 1;
    }

    public void Exit()
    {
        flag = 0;
    }
}

 

위 코드는 굉장히 단순하지만, 아쉽게도 정상 작동하는 코드는 아니다.

두 스레드가 while문을 동시에 통과할 경우 경쟁 상태가 발생할 수 있기 때문이다.

이를 해결하기 위해서는 CAS(Compare And Swap)라는 원자적 연산을 통해 flag를 체크하고 변경하는 두 작업을 단일 작업으로 만들어야 한다.

 

class MySpinLock
{
    int flag = 0; //0 = unlocked, 1 = locked

    public void Enter()
    {
        while (Interlocked.CompareExchange(ref flag, 1, 0) == 1); //CAS
    }

    public void Exit()
    {
        flag = 0;
    }
}

 

C#에서 CAS에 대응하는 메서드는 Interlocked.CompareExchange이다.

이를 활용하면 flag가 0인지 체크하고, 또 1로 변경하는 작업을 원자적으로 수행할 수 있다. flag가 1일 경우 변경하지 않는다.

CAS의 반환값은 flag의 기존값으로, 이는 lock 상태인지 아닌지를 확인하는 데에 사용된다.

 

메서드에 관한 자세한 설명은 아래 문서에 나와있다.

 

 

Interlocked.CompareExchange 메서드 (System.Threading)

두 값이 같은지 비교하여 같으면 첫 번째 값을 바꿉니다.

learn.microsoft.com

 


 

아래는 검증을 위한 코드이다.

 

class Program
{
    static int count = 0;
    static MySpinLock mySplnLock = new();

    static void Add()
    {
        for (int i = 0; i < 100000; i++)
        {
            mySplnLock.Enter();
            count++;
            mySplnLock.Exit();
        }
    }

    static void Sub()
    {
        for (int i = 0; i < 100000; i++)
        {
            mySplnLock.Enter();
            count--;
            mySplnLock.Exit();
        }

    }

    static void Main()
    {
        Thread t1 = new(Add);
        Thread t2 = new(Sub);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine(count);
    }
}

결과

정상적인 결과가 나오는 것을 확인할 수 있다.