실제 게임서버를 구현하다보면 위의 그림과 같은 상황이 펼쳐진다.

각 파트에 여러 직원(쓰레드)들이 존재하고 다른 파트와 협업을 할 수도 있다.

이때 Race Condition을 막기 위해 모든 작업에 Lock을 걸었다고 해보자. 과연 그게 옳은 선택일까??


MMORPG에서 보스몹 레이드를 할 경우 수백명의 유저들이 특정 지역으로 모이게 되어, 클라이언트 세션에서 게임로직으로 수 많은 패킷을 쏘게 될 것이다. 이 상황에서 Lock을 사용하면 어떻게 될까?

Lock은 상호배타적인 개념이다. 즉, 한 번의 작업을 처리할 수 있는 직원은 한 명뿐이라는 소리이다.

기껏 멀티 쓰레드 환경을 택해놓고 한 명이서 모든 작업을 처리하게금 구현을 해버린 것이다. 오히려 Lock을 하는 동안 문을 잠그고 푸는 시간까지 고려하면 멀티 쓰레드로 구현하는게 손해 인 셈이다.


멀티 쓰레드 환경에서 문제가 되는 것은 공용 공간의 데이터에 접근할 때 발생한다. Lock 역시 공용 공간의 데이터에 한 쓰레드만 접근하기 위해 사용하는 것이다.

그래서 우리는 Heap 영역과 데이터 영역처럼 전역 공간이지만!
스택처럼 각 쓰레드 별로 할당할 수 있는 저장 공간인 Thread Local Storage, 줄여서 TLS를 사용하겠다.

 class Program
    {
        static ThreadLocal<string> threadName = new ThreadLocal<string>();
        
        static void Whoami()
        {
            threadName.Value = $"My name is {Thread.CurrentThread.ManagedThreadId}";

            Thread.Sleep(1000); 

            Console.WriteLine(threadName.Value);
        }

        static void Main(string[] args)
        {
            Parallel.Invoke(Whoami, Whoami, Whoami, Whoami, Whoami); // 여기다 넣어주는 Action만큼 Task를 만들어준다. 
        }
    }

ThreadLocal<>를 이용하여 TLS를 만들 수 있다.

Parallel.Invoke의 경우 인자로 받은 Action 만큼의 Task를 자동으로 만들어주는 함수이다.

TLS에 저장된 threadName을 화면에 출력하면?

이런 식으로 각각의 쓰레드들의 이름이 잘 출력되는 것을 볼 수 있다.


하지만

        static string threadName; 
        
        static void Whoami()
        {
            threadName = $"My name is {Thread.CurrentThread.ManagedThreadId}";

            Thread.Sleep(1000); 

            Console.WriteLine(threadName);
        }

다음과 같이 코드를 수정하여 threadName을 공용공간 (static 변수이므로 데이터 영역에 들어간다)에 저장하게 되면

이런 식으로 현재 쓰레드의 아이디만 5번 출력하게 된다. TLS에 각쓰레드 마다 저장된 데이터가 아닌 공용공간에 저장된 데이터이기 때문이다.


코드를 다음과 같이 수정해보자.

        static ThreadLocal<string> threadName = new ThreadLocal<string>(() => 
        { return $"My name is {Thread.CurrentThread.ManagedThreadId}"; });
        
        static void Whoami()
        {
            bool IsRepeat = threadName.IsValueCreated; 

            if(IsRepeat)
            {
                Console.WriteLine($"My name is {threadName.Value}" + " (repeat)");
            } 
            else
                Console.WriteLine($"My name is {threadName.Value}");
        }

        static void Main(string[] args)
        {
            ThreadPool.SetMinThreads(1, 1);
            ThreadPool.SetMaxThreads(2, 2); 

            Parallel.Invoke(Whoami, Whoami, Whoami, Whoami, Whoami); // 여기다 넣어주는 Action만큼 Task를 만들어준다. 
        }

new ThreadLocal<>의 인자로 Func delegate를 받을 수 있다.

이 기능을 활용하면 TLS가 새로 만들어지면 ThreadLocal.Value에 return 값을 넣어주고
그대로 보관한다.

ThreadLocal.IsValueCreated는 Value가 초기화 되어졌으면 true, 초기화 되지 않았므면 false를 반환한다.


이 상황에서 SetMinThreads 와 SetMaxThreads 로 쓰레드 풀에서 뽑아오는 쓰레드의 갯수를 최소 1개, 최대 2개로 제한한다.

위의 사진을 통해 작업을 이미 완료한 쓰레드가 다시 돌아와서 Whoami 함수를 실행한다는 것을 확인할 수 있다.


profile
POSTECH EE 18 / Living every minute of LIFE

0개의 댓글