C# 서버

Race Condition(경합 조건), Atomic(원자성)

영춘권의달인 2024. 4. 29. 00:02
class Program
{
    static int number = 0;
    static void Thread_1()
    {
        for (int i = 0; i < 100000; i++)
            number++;
    }

    static void Thread_2()
    {
        for (int i = 0; i < 100000; 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(t1, t2);

        Console.WriteLine(number);
    }
}

위의 코드를 실행하면 어떤 결과가 나올까? 예상할 수 있는 결과는 0이다.

하지만 상대는 멀티쓰레드... 항상 예상하지 못한 결과가 나온다.

 

역시나 예상했던 0이 나오지 않고 실행할때마다 다른 결과가 나온다.

이것은 프로세서 2개가 동시에 공유 자원을 접근하기 때문에 예상하지 못한 결과가 나오는 것이고, 이것을 Race Condition(경합 조건)이라고 한다.

 

아니 그런데 number를 수정하는 코드는 단 한줄이고 한번에 일어나는 것인데 동시에 실행한다고 해도 상관없는 것 아닌가? 라는 의문이 들 수 있다.

하지만 어셈블리어로 분석해보면 number의 메모리 주소를 레지스터에 가져오고, 레지스터에서 연산을 하고, 

레지스터에서 연산한 결과를 다시 number의 메모리에 넣어주는 3단계의 과정을 거친다.

코드로 볼때는 한줄의 코드지만 실제로 실행이 될때는 3단계를 거치는 것이다.

 

그렇다면 이제는 위의 결과가 어떻게 나왔는지 유추할 수 있다.

1번 쓰레드에서 number의 메모리를 가져와서 레지스터에서 연산을 하는 도중에 2번 쓰레드에서 코드가 실행된다면 2번 쓰레드 역시 아직 연산이 완료되지 않은 number의 메모리를 가져와서 연산을 할 것이고, 두 쓰레드 중 하나의 결과로 덮어써지게 될 것이다. 

이런 현상이 여러번 일어나면서 위와 같이 예상하지 못한 결과가 나오게 된 것이다.

그리고 어셈블리의 한줄처럼 더이상 더 작은 작업으로 쪼개지지 못하는 작업을 atomic하다고 한다. (원자성)

 

위의 코드가 올바르게 동작하려면 number++과 number--가 atomic하게 동작해야 한다.

static void Thread_1()
{
    Interlocked.Increment(ref number);
}

 static void Thread_2()
 {
     Interlocked.Decrement(ref number);
 }

사용할 수 있는 방법중 하나는 Interlocked 함수를 사용하는 것이다.

이것은 number++, number--이 원자적으로 실행되도록 보장해주고 Race Condition 문제를 해결해준다.

이 Interlocked 함수는 내부적으로 메모리 배리어 기능을 제공하고 있기 때문에 가시성 문제는 발생하지 않는다.

하지만 한번에 하나의 쓰레드에서만 실행되야 하는 명령이 한줄이 아닐수도 있기 때문에 Interlocked함수는 사용하기 어렵고, Lock 등의 방법을 사용하는 것이 일반적이다.

'C# 서버' 카테고리의 다른 글

게임 입장  (0) 2024.05.04
서버 - Unity클라 통신 테스트  (0) 2024.05.03
Lock  (0) 2024.04.29
캐시  (1) 2024.04.28
멀티쓰레드 프로그래밍  (0) 2024.04.28