Home [멀티쓰레드] SpinLock
Post
Cancel

[멀티쓰레드] SpinLock

SpinLock

SpinLock은 임계 구역(Critical Section)에 진입이 불가능할 때 진입이 가능할 때까지 루프를 돌면서 재시도하는 방식으로 구현된 락을 가르킨다.

화장실문에 비유하자면 누가 화장실에 들어가 문을 잠궜을 때 내가 들어갈 수 있을 때까지 문을 열려고 시도하는 것이다.

SpinLock 구현

SpinLock을 구현하는 방법은 쉽지만 중요한 부분을 놓치기가 쉽다.

일단 그냥 단순히 생각나는대로 구현해보자.
먼저 인터페이스로는 들어가고 나오는 함수가 있어야 할 것이다.
그리고 들어가려고 할 때 이미 누가 lock을 걸어뒀다면 lock이 풀릴 때까지 루프 형식으로 기다려야할 것이다.
위 사항 그대로 구현해보면 아래와 같이 구현해볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SpinLock
{
    bool _locked = false;

    public void Enter()
    {
        while(_locked)
        {
            // 이미 잠겨 있으면 풀릴 때까지 기다린다.
        }

        _locked = true; // 잠군다.
    }

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

코드만 보았을 땐 별 문제없어 보인다. 그렇다면 정상적으로 lock 기능을 하는지 테스트해보자.

간단하게 두 쓰레드로 각각 num++, num–를 해주어 Race Condition이 발생하는지 확인해보면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int _num = 0;
static SpinLock _lock = new SpinLock(); // 직접 구현한 SpinLock

static void Thread_1()
{
    for (int i = 0; i < 100000; i++)
    {
        _lock.Enter(); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        _num++;

        _lock.Exit(); // 잠금을 푼다.
    }
}

static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        _lock.Enter(); // 자물쇠로 잠군다. (혹시나 잠겨 있으면 기다린다.)

        _num--;

        _lock.Exit(); // 잠금을 푼다.
    }
}

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);
}
1
-14365

정상적으로 동작했다면 0이 나와야하지만 결과로 0이 아닌 수가 나온것으로 보아 방금 구현해본 SpinLock에 문제가 있음을 짐작해볼 수 있다.

어떤부분이 문제였을까.

답부터 말하자면 Race Condition을 막기위해 SpinLock을 구현한 것이었지만 정작 방금 구현한 SpinLock 내부에서 Race Condition이 발생한 것이다.

예를 들어 지금 구현된 SpinLock에 두 쓰레드가 동시에 Enter()를 호출하였다고 해보자. 그러면 동시에 while문에서 _lock이 true인지 false인지 검사해볼 것이고 false이기 때문에 둘 다 거의 동시에 while문에서 빠져나와 _lock을 true로 바꾸고 임계 구역(Critical Section)에 들어가게 될 것이다.

이러한 문제가 발생한 이유는 Enter()에서 검사하는 행위, 잠그는 행위 이렇게 두 단계로 나눠서 처리하였기 때문이다.

그렇다면 이 두 단계를 한번에 처리를 한다면 해결될 것이다. 이전 포스트에서 언급한 원자적 연산을 이용하면 된다.

Race Condition과 Interlocked에 대한 내용

언어마다 보통 원자적 연산을 지원해주고 있으며 C#에서는 Interlocked를 이용하면 된다.

아까 SpinLock을 수정해보면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SpinLock
{
    int _locked = 0;

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

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

이해를 위해 Interlocked.CompareExchange(ref _locked, 1, 0) 부분을 해석해보자면 _locked가 0인 경우 1로 바꿔주며 반환은 바뀌기 전 원래 값을 반환해준다.
_lock이 0인 경우에는 0이기 때문에 1로 바뀌고 이전의 값 0이 반환된다.
_lock이 1인 경우에는 0이 아니기 때문에 바뀌지 않고 원래 값인 1이 반환된다.

이러한 연산을 CAS(Compare-And-Swap) 연산이라고 하며, 검사와 잠그는 행위를 원자적으로 한 번에 해줌으로써 아까 SpinLock의 문제를 해결한 것이다.
아까 전 테스트를 다시 해보면 0이 정상으로 출력되는 것을 볼 수 있을 것이다.

This post is licensed under CC BY 4.0 by the author.