본문 바로가기
Programming/C#

C# 스레드 동기화 문제 해결하기 디버그 모드와 릴리즈 모드에서의 차이점

by Dev_카페인 2024. 7. 1.
반응형

 

C# 스레드 동기화 문제 해결하기 디버그 모드와 릴리즈 모드에서의 차이점

문제 설명

다음은 문제의 예제 코드입니다. 디버그 모드에서는 정상적으로 종료되지만, 릴리즈 모드에서는 스레드가 종료되지 않는 문제가 발생합니다.

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();

            Thread.Sleep(1000);

            _stop = true;

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

            t.Wait();

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

Debug 모드의 동작
Release 모드에서의 동작

문제의 원인

이 코드는 bool 형식의 _stop 변수를 사용하여 스레드를 종료하도록 설계되었습니다. 그러나 릴리즈 모드에서는 컴파일러 최적화로 인해 예상치 못한 동작이 발생할 수 있습니다. 구체적으로는 _stop 변수에 대한 읽기와 쓰기 작업이 최적화되어 메모리에서 읽지 않고 CPU 캐시에서만 읽게 되는 경우가 발생할 수 있습니다. 이는 스레드가 메모리에서 _stop 변수의 변경 사항을 인식하지 못하게 합니다.

기계어로 변환되면서 최적화가 일어나는데 컴퓨터가 스레드로 동작한다는 것을 고려하지 않아서 일어나는 문제

해결 방법: volatile 키워드 사용

volatile 키워드는 변수의 값이 여러 스레드에 의해 변경될 수 있음을 컴파일러에게 알려줍니다. 이를 통해 컴파일러는 변수에 대한 읽기와 쓰기 작업을 최적화하지 않습니다. 즉, 항상 메모리에서 최신 값을 읽도록 보장합니다.

코드를 수정하여 _stop 변수를 volatile로 선언해보겠습니다.

class Program
    {
        static volatile 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();

            Thread.Sleep(1000);

            _stop = true;

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

            t.Wait();

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

이제 릴리즈 모드에서도 스레드가 정상적으로 종료될 것입니다.

다른 해결 방법: 동기화 프리미티브 사용

volatile 키워드는 간단한 경우에는 유용하지만, 복잡한 스레드 간 동기화가 필요한 경우에는 충분하지 않을 수 있습니다. 이런 경우에는 lock 문, Monitor, Mutex, ManualResetEvent 등의 동기화 프리미티브를 사용하는 것이 좋습니다.

lock 문을 사용한 해결 방법

lock 문을 사용하면 스레드 간의 상호 배제를 보장할 수 있습니다. 다음은 lock을 사용하여 문제를 해결하는 방법입니다.

class Program
    {
        static bool _stop = false;
        static readonly object _lock = new object();

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

            while (true)
            {
                lock (_lock)
                {
                    if (_stop)
                        break;
                }
            }

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

        static void Main(string[] args)
        {
            Task t = new Task(ThreadMain);
            t.Start();

            Thread.Sleep(1000);

            lock (_lock)
            {
                _stop = true;
            }

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

            t.Wait();

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

마치며

이번 포스트에서는 디버그 모드에서는 정상적으로 동작하지만 릴리즈 모드에서는 예상치 못한 동작이 발생하는 스레드 동기화 문제와 이를 해결하는 방법에 대해 알아보았습니다. volatile 키워드를 사용하여 간단하게 해결할 수 있는 경우가 있지만, 복잡한 동기화가 필요한 경우에는 lock 문과 같은 동기화 프리미티브를 사용하는 것이 좋습니다.

C#의 스레드 동기화에 대해 더 깊이 이해하고, 다양한 상황에 맞는 최적의 해결 방법을 선택하는 데 도움이 되기를 바랍니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

반응형