[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part4: Interlocked

참치와돌고래·2022년 11월 22일
0

컴파일러 최적화
오늘은 공유변수 접근에 대한 문제를 알아보도록 하자.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
    class Interlock
    {

        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(t1, t2);
            Console.WriteLine(number);
        }
    }
}

이번 시간에도 다음과 같은 코드를 작성하여 실행해보자. 이론상 t1에선 number 변수를 증가시키고, t2에선 감소시키니, 반복문 횟수도 똑같으니 결과는 0이 나올 것이다. 10000번 반복시 문제 없을 수 있지만 10만번, 100만번 반복시 우리가 원하는 방식으로 결과가 안될 것이다. 16424가 뜨거나 24394가 뜨거나... 어쨌건 0이 안나오는 사실에 당혹감이 들 것이다.

그러한 이유는 사실 증감 연산자는 원자성이 없기 때문.
어셈블리 코드를 보면 증감 연산자는 3줄의 코드로 이루어져 있다.
쉽게 말해

int temp=number;
temp+=1;
number=temp;
//=number++;

number++는 이와 같이 처리 된다는 점. 아시다시피 멀티쓰레드 환경에서는 어느 쓰레드가 먼저 실행될지는 우리도 모르고, 이와 같이 여러 줄의 코드가 단계별로 실행되니 뒤죽박죽이 되어 문제가 생기는 것이다.

아까 이야기했던 원자성은 그럼 무엇일까?
원자하면 물리 시간에 쪼개지지 않는 작은 존재...라고 얼핏 들어봤을 텐데 유사한 비유이다.
원자성이란 CPU가 명령어 1개로 처리하는 명령이어서, 도중에 다른 쓰레드가 끼어들 여지가 없고 실행이 되거나 OR 실행이 아예 안되거나로 존재할 수 있는 것을 의미한다.

그러니까 연산을 몇%만 수행했다! 이런 개념은 아예 없다.
RPG게임에서 상점 NPC에게 검을 100골드 주고 샀다고 가정해보자.

그렇다면 내 지갑에서 골드 -100, 내 인벤토리에 검 추가
와 같이 두 개의 명령을 통해 검이 추가될 것이다. 만약 이 작업 도중에 서버가 오류가 나버린다면? 골드를 지불하였음에도 검이 추가가 되지 않는 치명적인 버그가 발생할 것이다.

이 때문에 원자성이라는 성질이 필요하다. 우리는 지금 증감연산자에 대해 원자적으로 처리하고 싶으므로, 다음과 같은 명령어를 사용하면 된다.

Interlocked.Incremnt(ref number); //Number 변수값을 원자적으로 1 증가시킨다.
Interlocked.Decremnt(ref number); //Number 변수값을 원자적으로 1 감소시킨다.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
    class Interlock
    {

        static int number = 0;
        static void Thread_1()
        {

            for (int i = 0; i < 10000; i++)
            {
          
				Interlocked.Increment(ref number);
                //number++; 
            }
        }
        static void Thread_2()
        {
            for (int i = 0; i < 10000; i++)
            {
            	Interlocked.Decrement(ref number);
                //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);
        }
    }
}

그러나 Interlocked의 경우 정수 증감만 수행하므로 한계가 있는데, lock을 쓰게 되는데 이는 다음시간에 다루도록 한다.

0개의 댓글