공부/인프런 - Rookiss

Part 4-2-3. 멀티쓰레드 프로그래밍 : 컴파일러 최적화

셩잇님 2023. 9. 6. 01:13
반응형

 

 

멀티 쓰레드

 

컴파일러 최적화

using System;
using System.Threading;

namespace ServerCore
{
    class Program
    {
        static bool _stop = false;

        static void ThreadMain()
        {
            Console.WriteLine("쓰레드 시작!");

            while (_stop == false)
            {
                // 누군가 stop 신호를 해주기를 기다린다!
            }

            Console.WriteLine("쓰레드 종료!");
        }

        static void Main(string[] args)
        {
            // 메인에서 쓰레드를 생성하고 실행한다
            Task t = new Task(ThreadMain);
            t.Start();

            // 멈춘다
            _stop = true;

            Console.WriteLine("Stop 호출!");
            Console.WriteLine("종료 대기중!");
            t.Wait();

            Console.WriteLine("종료 성공!");
        }
    }
}

 

 개론 시간에서 얘기한 것 처럼 쓰레드는 아래 이미지와 같이 스택 메모리는 각자 할당 받지만, 전역 변수는 모든 스레드들이 공통으로 같이 사용한다.

 

 

 그렇다면 쓰레드가 static으로 선언한 bool 값과 Thread 메서드에 동시 접근할 때 무슨일이 일어날까? 이를 확인하기 위해 Task를 이용하여 스레드 풀에 있는 스레드를 이용해 일감(=ThreadMain)을 분배받아 t.Start()를 실행한다.

 

 또한 태스크를 정상적으로 실행시키기 위해 메인 쓰레드를 대기 시킨다. 이를 위해 Task.Sleep() 메서드를 이용한다. 인자값은 밀리세컨드 단위를 사용하므로 값 '1000'은 1초를 의미한다. 주로 N초간 대기를 실행시키기 위해 사용된다.

 

 그리고 대기가 끝난다면 전역 변수로 선언한 _stop을 true로 바꿔주어 쓰레드를 종료시켜준다. 그리고 종료가 정상적으로 되었는지를 확인하기 위해 t.Wait() 메서드를 이용하여 테스크가 정상적으로 끝났는지를 확인한다. 현재는 테스크를 이용하기 때문에 t.Wait()메서드를 사용하지만 만약 쓰레드일 경우에는 t.Join() 메서드를 사용해야 한다.

 

 정말 마지막으로 쓰레드의 호출 순서를 알아보기 위해 로그를 남겨준다. 프로그램을 실행해보면 정상적으로 실행된 것을 알 수 있다. 그러나 과연 이것이 괜찮을까?

 


 

Debug / Release

 우리는 여태껏 Debug 모드를 통해 코드를 실습하고 사용해왔다. 그러나 실질적인 게임 배포는 Release 모드로 이루어진다. 이 릴리즈 모드를 사용한다면 최적화가 이루어져 프로그램이 빨라진다. 즉 이 말은 디버깅하기가 어려워진다는 말이다. 과도한 최적화로 인해 브레이크 포인트를 걸어도 잡히지가 않는 것이다.

 

 똑같은 코드를 릴리즈 모드 실행해보자. 놀랍게도 코드가 바뀐 것이 하나도 없는데 릴리즈 모드로 소스코드를 실행하면 무한 루프에 빠져버린다.. 😭 도대체 어떻게 최적화를 진행한 것일까..? 이는 현업에서도 멀티쓰레드로 작업을 진행하다보면 비일비재 한다고 하신다. 정상적으로 동작하는 프로그램도 라이브로 출시를 하면 버그가 나타나는 등.. 그럼 도대체 뭐가 문제일까? 

 

 이를 확인하기 위해 while문 내부에 브레이크 포인트를 걸고 실행해보자. 이 때 디버그 - 창 - 디스어셈블리 탭을 열어서 확인해보자. 어셈블리 언어란 컴퓨터랑 가장 가까운 저급언어이다. 

 

디스 어셈블리 창으로 확인한 소스코드.

 

        static void ThreadMain()
        {
            Console.WriteLine("쓰레드 시작!");

            while (_stop == false)
            {
                // 누군가 stop 신호를 해주기를 기다린다!
            }
            
            // 컴파일러가 아래와 같이 수정해버림 😩
            if (_stop == false)
            {
                while (true)
                {

                }
            }

            Console.WriteLine("쓰레드 종료!");
        }

 

 어셈블리 언어를 우리가 일기쉽게 소스코드로 수정하면 아래의 if 문과 같다. 즉 컴파일러가 우리를 위해 최적화를 하면 기뻐할 것이라고 생각하고 아래와 같이 수정한 것이다. 소스코드를 분석해보니 _stop = faslse 인데, _stop의 값을 변경하는 로직이 없으니 if문과 같이 수정한 것이다. 따라서 릴리즈 모드에서는 동일한 소스코드가 무한 루프에 갇혀 버리게 된다.

 


 

최적화를 안하는 방법

 다양한 방법이 있지만 간단한 방법은 volatile 키워드를 이용하는 것이다. 이는 컴파일러에게 _stop의 값이 언제 변할지 모르니 최적화를 하지 말아라! 라고 명령하는 것이다.

 

volatile static bool _stop = false;

 

참고로 볼라티 키워드는 C++과 C#에도 존재하지만 서로 의미가 다르다. C++에서는 마찬가지로 '최적화 하지마!' 라는 의미이지만, C# 에서는 캐시를 무시하고 최신값을 가져오는 것 뿐만 아니라 다양한 기능도 사용된다. 따라서 전문가들은 볼라티를 사용하는 것이 아닌 락이나, 아토믹을 사용하는 것을 권장한다.

 

 

 

반응형