공부/인프런 - Rookiss

Part 4-2-15. 멀티쓰레드 프로그래밍 : TLS(Thread Local Storage)

셩잇님 2023. 9. 18. 19:00
반응형

 

 

멀티 쓰레드

 

TLS(Thread Local Storage)

 쓰레드마다 고유하게 접근할 수 있는 전역 변수.

 

TLS는 왜 필요할까? PPT와 예제를 통해 알아보잣! 🤠

 

 

 우리는 락을 이용해 화장실처럼 동시 다발적으로 사용해야 하는 공간을 자물쇠를 이용해 사용하는 방법에 대해서 알아보았다. 여기까지만 보면 아름답지만, 현실은 녹록치 않다. 왜냐하면 주방에서 일어나는 일, 결제, 손님 테이블에서 일어나는 일 모두 각각의 직원이 대략적으로 현재 상황이 어떻게 이뤄지고 있는지 알아야 되기 때문이다. 즉 위의 이미지의 화살표처럼 일이 일사천리로 수월하게 처리되는 것이 아니라는 것이다.

 

 그렇다면 락을 이용해 경합이 일어날 만한 곳에 락을 잡으면 되지 않을까? 생각할 수 있다. 

 

식당 예제로 알아보는 연관성

 

 하지만 이도 녹록치 않다. 왜냐하면 직원들과의 동기화 작업이 필요하기 떄문이다. 주방 직원이 2명이 있는데 두 직원 도무 왼쪽이미지의 주방 환경에서 위에서 3번째와 같은 공간을 이용하기 위해 한 공간에 둘이 들어가 일을 하고 있어서 나머지 공간들의 여유가 있음에도 불구하고 멍청하게 한 공간에 둘이 들어가기 때문이다. 즉 일감 분배를 어떻게 해야하는지가 멀티 쓰레드의 핵심이다.

 

 이를 게임으로 다시 설명한다면 아래와 같다. 게임로직, DB, 클라이언트 세션 모두 연동이 되어있고 연관성이 있는 것이다. 로직을 실행하면 DB에 저장해도 해야하고, 클라이언트에게도 보여줘야하기 때문이다. 결국 모든 로직이 서로 연관성있게 얽혀있는 것이다. 

 

 그렇다면 락을 하나씩 배치해서 실행하면 해결될 수 있지않을까? 라는 문제도 결국 정답이 될수 없다는 것이다. 즉 아래와 같이 한쪽에 직원들이 몰리게 되는 현상이 발생하는 것이다. 

 

 

 예를 들어 롤에서 5:5 한타를 한다고 가정해보자. 우리는 미드 1차 타워 앞에서 5명이 모여서 싸움을 진행하게 된다. 그러면 탑, 바텀, 정글과 같은 맵 다른 지역은 아무런 유저가 없어서 한가하게 될 것이고, 미드 앞에서만 10명이 모이기 때문에 이의 이미지와 같이 직원들이 게임 로직에 모두 붙어있게 된다.

 

 그렇지만 락의 특성상 상호배타적인 개념이기 때문에 아무리 직원들이 열심히 몰려서 처리하고 싶다고 해도, 한번에 한명씩, 한번밖에 처리할 수 없기 때문에 문제가 된다. 즉 멀티 쓰레드를 사용한 보람이 없어지는 것이다. 따라서 멀티 쓰레드 환경이라고 해서 락을 이용하는 것이 최선이 아님을 명심해야 한다는 것이다. 따라서 이러한 문제를 해결하기 위해서 TLS를 이용하는 것이다.

 

 

 이전에 배운 쓰레드의 힙 영역과 스택 영역에 대해서 다시 알아보면 쓰레드는 힙(heap)영역과 데이터 영역을 공유하면서 사용하고, 스택(stack) 영역은 각각의 독립된 공간에서 사용한다고 알고 있다. 그러나 이 때 TLS 기능을 이용하면 쓰레드간의 공유 - 개인 영역 사이에 새로운 공간을 만들어 줄 수 있다. 이는 전역으로 설정하기에는 부담스럽지만 각 쓰레드들이 공통으로 사용될 만한 새로운 영역을 쓰레드에게 제공해주는 것이다. 이제 코드를 이용해 TLS를 알아보자.

 

 


 

 

TLS 구현

class Program
{
    // 스레드별로 이름을 붙여준다.
    // static string ThreadName;

    // 아래와 같이 매핑하여 사용한다.
    static ThreadLocal<string> ThreadName = new ThreadLocal<string>();

    static void Main(string[] args)
    {
    
    }
}

 

 만약 쓰레드별로 고유의 이름을 붙여주는 일을 한다고 생각해보자. 기존과 같이 static string을 이용하여 변수를 선언하면 다른 쓰레드에서 해당 값을 Write 하기 때문에 정상적으로 쓰레드에게 고유의 이름을 붙일 수 없다. 따라서 TLS의 기능을 이용하기 위해서는 ThreadLocal을 이용하여 변수를 생성해야 한다.

 

    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, WhoAmI, WhoAmI);
        }

 

 WhoAmI라는 메서드를 생성하여 각각의 쓰레드가 정말로 고유의 공간을 가지는지 확인하여 보자. 이 때 메인함수에서는 Parallel 이라는 처음 보는 클래스를 사용하는데 검색해보니 Parallel은 병렬 처리를 위해 사용한다고 나와있다. 이를 통해 실행해보면 아래와 같은 결과가 나온다. 

 

 

 그러나 위와 같은 방법으로 TLS를 호출하여 사용한다면 _threadLocal.Value에 이미 Id 값이 들어가있어도 WhoAmI() 메서드가 실행될 때마다 새로운 값을 이용해 덮어 씌워 불필요한 행동을 하게 된다. 이를 해결하기 위해 아래와 같이 코드를 수정해보자.

 

class Program
{
    // 람다를 이용하여 생성한다.
    static ThreadLocal<string> ThreadName = new ThreadLocal<string>(() => { return $"My Name is {Thread.CurrentThread.ManagedThreadId}"; });

    static void WhoAmI()
    {
        bool repeat = ThreadName.IsValueCreated;
        if (repeat)
            Console.WriteLine(ThreadName.Value + " repeat ");
        else
            Console.WriteLine(ThreadName.Value);
    }

    static void Main(string[] args)
    {
        ThreadPool.SetMinThreads(1, 1);
        ThreadPool.SetMaxThreads(3, 3);
        Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI);
    }
}

 

 먼저 기존에 생성하던 ThreadName을 람다를 이용해 생성하는 형태로 변경하고, bool repeat를 활용하여 이미 제작된 Id일 경우 새롭게 생성하지 않고 넘어갈 수 있또록 구현하자. IsValueCreated 메서드를 활용한다면 이미 제작되어 true의 값을 리턴하도록 하여 중복된 Id일 경우를 구분할 수 있다. 따라서 위와 같이 코드를 변경한다면 Id를 계속 생성하는 형태가 아닌 값을 체크 후 없으면 새롭게 만들고, 있을 경우 그대로 출력하는 형태로 만들 수 있다. 😎

 

 

 

반응형