Section1: SpinLock

kmjvelog·2023년 11월 11일

[C# 서버] 

목록 보기
5/10

SpinLock

class SpinLock
{
    volatile bool _locked = false; // false: 아무도 사용하고 있지 않다. 
    public void Acquire() // 획득하겠다.
    {
        // 락이 풀릴때까지 기다려야 한다.
        while (_locked)
        {

        }

        _locked = true;
    }

    public void Release() // 획득한것을 내려놓겠다. 
    {
        _locked = false;
    }
}

이렇게 할 수 있다. (하지만 문제가 있다.)
나머지 코드는 아래와 같다.

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

    static void Thread_1()
    {
        for(int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num++;
            _lock.Release();
        }
    }
    static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Acquire();
            _num--;
            _lock.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);
    }
}

문제

위 코드는 문제가 있다. 최종 _num값이 0이 나오지 않는다. 그 이유른 두 스레드가 또 거의 동시에 실행될때 발생하는 것이다.

while (_locked)
{

}

_locked = true;

이부분이 문제 인 것이다. 두 스레드가 거의 동시에 실행되고 있어 _locked를 아무도 true로 바꾸지 않고 while문을 넘어가면 문제가 되는 것이다. 스레드는 누가 같이 _locked키를 얻었는지 관심이 없다. 그저 키를 획득했으면 자신의 일을 할 뿐이다.

해결방법

원자성과 관련이 있다.

while (_locked)
{

}

_locked = true;

이 부분은 원자적으로 실행되어야한다.

Interlocked계열의 함수들을 이용해보자.

// _locked를 int로 바꿔야지 아래 코드가 동작한다.
public void Acquire() 
{
    while (true)
    {
        int original = Interlocked.Exchange(ref _locked, 1);
        if (original == 0)
            break;
    }
}

이렇게 하면 _num의 최종값이 0이 된다. Interlocked.Exchange이 함수는 단지 _locked의 값을 1로 바꾸는 것이다. 반환값이 중요한데 반환값의 의미는 내가 바꾸기 이전 값을 의미한다. 0이 반환되면 아무도 이 키를 가지고 있지 않았다는 의미고 1이 반환되면 누군가 이 키를 가지고 있다는 의미이다.
이렇게 하면 문제는 해결되지만 직관적이지 않고 경우에 따라 위험할 수 있다.

Interlocked.Exchange의 의미는

int original = _locked;
_locked = 1;

이런 의미인데 사실 이것보다는

if(_locked == 0)
	_locked = 1; 

이것이 더 직관적이다.
그리고 경우에 따라 위험한 이유는 무작적 1로 대입한다는 것이다.

Interlocked의 다른 함수가 하나 더 있다.

while (true)
{
    // 첫 인자와 세번째 인자를 비교한다. 같으면 두번째 인자로 바꾼다. 
    int original = Interlocked.CompareExchange(ref _locked, 1, 0);
    if (original == 0)
        break;
}

이런 방식을 이런 방식을 CAS(Compare-And-Swap)방식이라고 한다. 비교 후 변경해 준다는 것을 의미한다.

코드를 좀 더 정리하면 이렇게 쓸 수 있는데 이렇게 쓰는 것이 좀 더 직관이기도 하다.

int expected = 0; // 예상되는
int desired = 1; // 원하는            
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
    break;
profile
너무너무화가난다.똑같은게시글이3개나올라가서하나삭제했더니다삭제되었다.노션으로간다.

0개의 댓글