게임 서버 이론 구현 #2 - Interlocked

Interlocked

Interlocked는 쓰레드를 사용할 때 변수의 원자성[각주:1]을 보존해주는 명령어다.

변수의 원자성이란 다음 코드를 보면서 알아보자.

static int number = 0;

static void Thread_1()
{
    for (int i = 0; i < 10000; i++)
        number++;
}

static void Thread_2()
{
    for (int i = 0; i < 10000; i++)
        number--;
}


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

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

    Task.WaitAll();

    Console.WriteLine(number);
}

흐름을 보면 number라는 변수가 있고,

2개의 쓰레드에서 number에 접근해 하나는 값을 하나씩 올리고, 하나는 값을 하나씩 내리는 코드다.

만번 씩 값을 오르내리는 정도는 문제 없지만 해당 변수에 여러 쓰레드가 기하급수적으로 많이 접근하게 되면 문제가 발생하게 된다.

여러 쓰레드가 하나의 메모리에 동시에 접근하여 값이 꼬이는 현상이 바로 경합조건이다.

이런 경합 조건이 발생하는 걸 막기 위해 Interlocked라는, 원자성을 보존해주는 클래스가 등장하게 된다.

우선 증감 연산자는 원자성을 보존해주는 연산자가 아니다.

우리가 보는 코드 상에는 단순히 ++, --로 끝나지만 실제로는

int tmp = number; // 해당 변수의 레지스터에서 값을 불러옴
tmp += 1; // 값에다가 +1 연산해주기
nubmer = tmp;  // 다시 레지스터에 변한 값을 저장

이런 3가지 과정을 거쳐야 한다.

그렇기 때문에 3번 과정이 일어나려고 할 때 다른 쓰레드가 접근해버리면 의도한 결과와는 다른 값이 나오게 된다.

Interlocked는 레지스터 주소값에 직접 접근해 값을 바꿔버리기 때문에 과정이 하나 밖에 안 일어나고 원자성이 보장된다.

static int number = 0;

static void Thread_1()
{
    for (int i = 0; i < 100000; i++)
    {
        Interlocked.Increment(ref number);
    }
}

static void Thread_2()
{
    for (int i = 0; i < 100000; i++)
    {
        Interlocked.Decrement(ref number);
    }
}


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

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

    Task.WaitAll();

    Console.WriteLine(number);
}

Interlocked 클래스에서 제공하는 변수는 정수 밖에 없으니 유의해서 사용하자.

https://learn.microsoft.com/ko-kr/dotnet/api/system.threading.interlocked?view=net-7.0#methods

 

Interlocked 클래스 (System.Threading)

다중 스레드에서 공유하는 변수에 대한 원자 단위 연산을 제공합니다.

learn.microsoft.com

 

  1. 어떤 것이 더이상 쪼개질 수 없는 성질 [본문으로]