(C#) SpinLock
포스트
취소

(C#) SpinLock

SpinLock이란?

SpinLock은 만약 다른 스레드가 lock을 소유하고 있다면 그 lock이 반환될 때까지 계속 확인하며 기다린다.

💻 코드

[ Source Code (Click) ]
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class SpinLock
{
    volatile bool _locked = false;

    // Enter
    public void Acquire()
    {
        // 잠김이 풀리기를 기다린다.
        while (_locked == true)
        {
            
        }

        // 내꺼!
        _locked = true;
    }

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

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이 아니라 아에 다른값이 나오는 것을 확인할 수 있다.
이러한 문제를 해결하기 위해 두가지 방법이 있다.

  • Interlocked.Exchange(ref location, value);
  • Interlocked.CompareExChange(ref location, desired, expected);

Interlocked.Exchange

Interlocked.Exchange를 코드로 풀어보면 아래와 같은 느낌이다.

1
2
3
4
5
6
7
8
9
10
public void Acquire()
{
    while (true)
    {
        int original = _locked;
        _locked = 1;
        if (original == 0)
            break;
    }
}

하지만 이렇게 구현 한다면 두줄의 코드를 거쳐 실행되기 때문에 공동으로 사용되는 _locked가 다른 스레드에서 사용될 수도 있다.

이 때문에 한줄로 구현해줄 필요가 있으며 구현 방법은 다음과 같다.


1
2
3
4
5
6
7
8
9
public void Acquire()
{
    while (true)
    {
        int original = Interlocked.Exchange(ref _locked, 1);
        if (original == 0)
            break;
    }
}

Interlocked.Exchange(ref _locked, 1)가 실행되면 _locked에 1을 넣게된다.

Interlocked.Exchange의 반환값은 _locked에 1이 들어가기 전의 값을 반환해준다.

Interlocked.CompareExchange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void Acquire()
{
    while (true)
    {
        // CAS Compare-And-Swap
        // Interlocked.CompareExchange를 풀어보면 아래와 같은 느낌이다.
        // if (_locked == 0)
        //     _locked = 1;
        // 인자값을 넘겨줄 때 0, 1로 넘겨줘도 되지만 이해하기 슆게 expected, desired를 선언했다.
        int expected = 0;
        int desired = 1;
        if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
            break;
    }
}

Interlocked.CompareExChangeInterlocked.Exchange와 비슷하지만 조금 더 정교하다.

사용법은 _locked의 이전값이 expected라면 _locked에 desired 값을 반환한다.

리턴값은 Exchange와 똑같이 이전 값을 리턴한다.


기다린 후 재요청하는 3가지 방법.

  1. Thread.Sleep(1); : 무조건 휴식 -> 무조건 1ms 정도 쉰다.
  2. Thread.Sleep(0); : 조건부 양보 -> 나보다 우선순위가 낮은 애들한테는 양보 불가 -> 우선순위가 나보다 같거나 높은 쓰레드가 없으면 본인이 선택됨.
  3. Thread.Yield(); : 관대한 양보 -> 관대하게 양보할테니, 지금 실행이 가능한 스레드가 있으면 실행 -> 실행 가능한 애가 없으면 남은 시간 소진.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
volatile bool _locked = false;

// Enter
public void Acquire()
{
    while (true)
    {
        if (Interlocked.CompareExChange(ref _locked, 1, 0) == 0)
            break;

        // 상황에 맞게 사용.
        Thread.Sleep(1); 
        Thread.Sleep(0); 
        Thread.Yield();       
    }
}

기본적으로 OS 단에 요청하는 거의 모든 API (Console.Write, Sleep 등)은

CPU 사용권을 일단 반납하고 운영체제 쪽에서 다음 처리를 판별하게 되기 때문에 Context Switching이 일어난다.


💡 참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

(C#) DeadLock

(C#) 컨텍스트 스위칭과 Event