본문 바로가기
공부/게임서버

SpinLock / Interlocked.Exchange

by samosa 2024. 11. 10.

문제 코드

namespace ServerCore;

internal class SpinLock
{
    private volatile int _locked;

    public void Enter()
    {
        while (_locked == 0)
        {

        }

        _locked = 1;
    }

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

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

    public static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Enter();
            _num++;
            _lock.Leave();
        }
    }

    public static void Thread_2()
    {
        for (int i = 0; i < 100000; i++)
        {
            _lock.Enter();
            _num--;
            _lock.Leave();
        }
    }

    private static void Main(string[] args)
    {
        var t1 = new Task(Thread_1);
        var t2 = new Task(Thread_2);
        t1.Start();
        t2.Start();

        Task.WaitAll(t1, t2);

        Console.WriteLine(_num);
    }
}

이 코드의 주요 문제점은 SpinLockEnter 메서드 구현에 있다. 현재 구현은 Enter 메서드가 적절한 방법으로 원자성을 보장하지 않기 때문에 다중 스레드 환경에서 안전하지 않다. _locked == 0 조건을 검사하고 _locked = 1로 설정하는 과정이 원자적으로 실행되지 않아 경쟁 상태(race condition)가 발생할 수 있다.

이로 인해 다음과 같은 상황이 발생할 수 있다:

  • 두 스레드가 동시에 _locked == 0 조건을 만족하고, _locked = 1로 변경되기 전에 다른 스레드가 끼어들어 _locked 값을 조작할 수 있다.
  • 결과적으로 SpinLock이 제대로 작동하지 않고 여러 스레드가 동시에 임계 구역에 들어가게 된다.

해결 방법

이 문제를 해결하기 위해 Interlocked.Exchange 메서드를 사용하여 _locked 값을 원자적으로 교환해야 한다. Interlocked.Exchange는 스레드 간 원자성을 보장하므로 경쟁 상태를 방지할 수 있다.

수정된 Enter 메서드는 다음과 같다:

public void Enter()
{
    // 원자적 교환을 통해 잠금 상태를 설정한다.
    while (Interlocked.Exchange(ref _locked, 1) == 1)
    {
        // 다른 스레드가 잠금을 가지고 있는 동안 대기한다.
    }
}

while (true) 루프 안에 있는 Interlocked.Exchange 메서드는 스레드가 _locked 변수를 1로 설정하는 원자적 연산을 수행하며, 현재 _locked의 값에 따라 잠금 상태를 제어한다.

코드의 주요 부분을 다시 보면 다음과 같다:

while (true)
    if (Interlocked.Exchange(ref _locked, 1) == 0)
        return;

이 코드의 동작을 하나씩 설명하면:

  1. Interlocked.Exchange: Interlocked.Exchange(ref _locked, 1)_locked 변수의 값을 1로 변경하는 동시에, _locked이전 값을 반환한다. 이 연산은 원자적으로 수행되기 때문에 여러 스레드가 동시에 이 연산을 실행해도 충돌 없이 동작한다.
  2. 잠금 확인과 설정:
    • _locked의 이전 값이 0(잠기지 않은 상태)일 경우, Interlocked.Exchange0을 반환하고 _locked1로 설정한다. 이 상태에서는 현재 스레드가 잠금을 획득하게 되므로, return을 통해 Enter 메서드가 종료된다.
    • _locked의 이전 값이 1(이미 잠금 상태)일 경우, Interlocked.Exchange1을 반환한다. 이 경우 if 조건이 만족되지 않아 return하지 않고 while 루프가 반복된다.
  3. 루프 동작: 다른 스레드가 Leave 메서드를 통해 _locked0으로 설정할 때까지 이 while 루프는 계속 반복된다. _locked0이 되면 Interlocked.Exchange에서 0을 반환하고 _locked1로 변경하면서 잠금을 획득하게 된다.

간단히 요약하면

  • Interlocked.Exchange(ref _locked, 1)_locked 값을 원자적으로 1로 설정하면서 이전 값을 반환하여, _locked0인지 확인하고 1로 설정하는 작업을 원자적으로 수행한다.
  • _locked0일 때만 스레드가 잠금에 성공하고, 그렇지 않으면 while 루프를 통해 잠금이 풀릴 때까지 대기하게 된다.

이렇게 구현된 코드 구조는 여러 스레드가 동시에 잠금에 접근하려 할 때 안전하게 하나의 스레드만 잠금을 획득하도록 보장한다.


원자적? 원자성?

'원자적' 혹은 '원자성'은 컴퓨터 과학에서 어떤 작업이 더 이상 분할될 수 없는 최소 단위로 수행된다는 것을 의미한다. 즉, 작업이 중간에 중단되거나 다른 작업이 끼어들 수 없는 상태를 말한다. 원자적 연산은 반드시 완전히 실행되거나 전혀 실행되지 않는 방식으로 수행된다.

'원자적'이라는 표현은 화학의 '원자'에서 유래한 것으로, 더 이상 쪼갤 수 없는 단일 단위를 가리킨다. 다중 스레드 환경에서는 여러 스레드가 공유 자원을 동시에 접근할 때 데이터 무결성을 보장하기 위해 원자적 연산이 필요하다.

 


while문 부분을 이렇게도 쓸 수 있다.

while (true)
        {
            int original = Interlocked.Exchange(ref _locked, 1);
            if (original == 0)
                break;
        }

 

이 코드에서 original은 스레드의 스택 메모리에 위치한다. 스택 메모리는 각 스레드가 독립적으로 사용하는 메모리 공간이기 때문에, 다른 스레드와 메모리 접근 충돌이 발생하지 않는다.

 

'공부 > 게임서버' 카테고리의 다른 글

임계영역 (Critical Section)  (0) 2024.11.10
가시성 문제와 보장 방법  (0) 2024.11.10
컨텍스트 스위칭  (0) 2024.11.10
교착 상태 (Deadlock)  (1) 2024.11.09
Interlocked 사용 시 그 반환값을 사용하자  (0) 2024.11.09