C# 다중 스레딩: 스레드 제어, 상태, 우선순위 및 유형 이해

스레드 중단 (Abort)

스레드 중단은 스레드의 실행을 강제로 멈추는 기능입니다. C#에서는 Thread.Abort() 메서드를 제공했으나, 이 메서드는 예측 불가능한 동작을 유발하고 리소스 누수를 일으킬 수 있어 .NET 5.0부터는 더 이상 지원되지 않으며 사용을 권장하지 않습니다. 대신 협력적 취소(Cooperative Cancellation) 패턴을 사용하는 것이 좋습니다. 여기서는 레거시 API의 동작을 이해하기 위한 예시를 살펴봅니다.


using System;
using System.Threading;

namespace ThreadControlApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("메인 스레드 시작.");
            Thread workerThread = new Thread(PerformInterruptibleWork);

            workerThread.Start();
            Thread.Sleep(TimeSpan.FromSeconds(5)); // 5초 대기 후 중단 시도

            // Thread.Abort()는 현재 .NET 버전에서 지원되지 않거나 권장되지 않습니다.
            // 이 코드는 이전 .NET Framework 환경에서 동작 방식을 보여주기 위함입니다.
            try
            {
                // .NET 5.0 이상에서는 PlatformNotSupportedException을 발생시킬 수 있습니다.
                workerThread.Abort();
                Console.WriteLine("작업자 스레드에 중단 요청을 보냄.");
            }
            catch (PlatformNotSupportedException)
            {
                Console.WriteLine("경고: 현재 플랫폼에서는 Thread.Abort()가 지원되지 않습니다.");
            }
            catch (ThreadStateException)
            {
                Console.WriteLine("경고: 스레드가 이미 종료되었거나 중단할 수 없는 상태입니다.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"예외 발생 중: {ex.Message}");
            }

            Console.WriteLine("메인 스레드 계속 진행.");

            Thread.Sleep(TimeSpan.FromSeconds(2)); // 잠시 대기하여 중단 처리 확인

            // 새로운 작업 스레드 시작
            Thread anotherThread = new Thread(PerformSimpleWork);
            anotherThread.Start();
            PerformSimpleWork(); // 메인 스레드에서도 같은 작업 수행

            Console.WriteLine("모든 작업 완료. 아무 키나 눌러 종료.");
            Console.ReadKey();
        }

        /// <summary>
        /// 2초마다 진행 상황을 출력하며 반복하는 메서드
        /// </summary>
        static void PerformInterruptibleWork()
        {
            Console.WriteLine("[인터럽트 가능 작업] 스레드 시작. 현재 시간: " + DateTime.Now.ToLongTimeString());
            for (int k = 0; k < 10; k++)
            {
                Console.WriteLine($"[인터럽트 가능 작업] 진행 중... {k}번째. 시각: {DateTime.Now.Second}초");
                try
                {
                    Thread.Sleep(TimeSpan.FromSeconds(2));
                }
                catch (ThreadAbortException)
                {
                    Console.WriteLine("[인터럽트 가능 작업] ThreadAbortException 발생! 스레드 종료.");
                    // Thread.ResetAbort(); // 선택적으로 Abort를 취소할 수 있으나, 일반적으로는 스레드가 종료됨
                    break; // 루프 종료
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[인터럽트 가능 작업] 예외 발생: {ex.Message}");
                    break;
                }
            }
            Console.WriteLine("[인터럽트 가능 작업] 스레드 종료.");
        }

        /// <summary>
        /// 간단한 숫자 출력 작업 메서드
        /// </summary>
        static void PerformSimpleWork()
        {
            Console.WriteLine("[간단 작업] 스레드 시작. 시각: " + DateTime.Now.ToLongTimeString());
            for (int j = 0; j < 5; j++)
            {
                Console.WriteLine($"[간단 작업] 숫자: {j}");
            }
            Console.WriteLine("[간단 작업] 스레드 종료.");
        }
    }
}

이 예시에서 PerformInterruptibleWork 스레드가 시작된 후, 메인 스레드는 5초 후에 workerThread.Abort()를 호출하여 스레드 중단을 시도합니다. 스레드가 중단되면 ThreadAbortException이 발생하며, 이를 통해 스레드를 안전하게 종료할 수 있습니다. 하지만 이 방법은 스레드 내부 상태를 예측하기 어렵게 만들고, finally 블록이 실행되지 않아 리소스 누수로 이어질 수 있기 때문에 사용을 지양해야 합니다.

스레드 상태 (ThreadState)

각 스레드는 생명 주기 동안 다양한 상태를 가집니다. ThreadState 열거형은 스레드의 현재 실행 상태를 나타내며, 이는 디버깅 및 스레드 관리 시 유용하게 활용될 수 있습니다. 다음은 스레드 상태를 모니터링하는 예시입니다.


using System;
using System.Threading;

namespace ThreadStateMonitoringApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("메인 스레드 시작: 스레드 상태 모니터링");

            Thread longRunningThread = new Thread(ExecuteLongRunningTask);
            Thread quickTaskThread = new Thread(ExecuteQuickTask);

            // 스레드 시작
            quickTaskThread.Start();
            longRunningThread.Start();

            // 특정 스레드의 상태를 주기적으로 출력
            for (int i = 0; i < 15; i++)
            {
                Thread.Sleep(TimeSpan.FromMilliseconds(200));
                Console.WriteLine($"긴 작업 스레드 상태 ({i}번째): {longRunningThread.ThreadState}");
                if (longRunningThread.ThreadState == ThreadState.Stopped) break; // 스레드가 종료되면 일찍 루프 종료
            }

            Console.WriteLine("\n잠시 후 긴 작업 스레드에 중단 요청 시도...");
            Thread.Sleep(TimeSpan.FromSeconds(3));

            try
            {
                // Thread.Abort()는 현재 .NET 버전에서 지원되지 않거나 권장되지 않습니다.
                longRunningThread.Abort();
                Console.WriteLine("긴 작업 스레드에 Abort 요청 완료.");
            }
            catch (PlatformNotSupportedException)
            {
                Console.WriteLine("경고: 현재 플랫폼에서는 Thread.Abort()가 지원되지 않습니다.");
            }
            catch (ThreadStateException)
            {
                Console.WriteLine("경고: 스레드가 이미 종료되었거나 중단할 수 없는 상태입니다.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"예외 발생 중: {ex.Message}");
            }


            // 최종 스레드 상태 출력
            Thread.Sleep(TimeSpan.FromSeconds(1)); // 중단 처리 시간 대기
            Console.WriteLine($"\n[최종 상태] 긴 작업 스레드: {longRunningThread.ThreadState}");
            Console.WriteLine($"[최종 상태] 짧은 작업 스레드: {quickTaskThread.ThreadState}");

            Console.WriteLine("모든 모니터링 완료. 아무 키나 눌러 종료.");
            Console.ReadKey();
        }

        /// <summary>
        /// 짧게 실행되고 종료되는 스레드 작업
        /// </summary>
        static void ExecuteQuickTask()
        {
            Console.WriteLine("[짧은 작업 스레드] 시작. 현재 상태: " + Thread.CurrentThread.ThreadState);
            Thread.Sleep(TimeSpan.FromSeconds(1.5)); // 1.5초 대기
            Console.WriteLine("[짧은 작업 스레드] 작업 완료.");
        }

        /// <summary>
        /// 주기적으로 숫자를 출력하며 오래 실행되는 스레드 작업
        /// </summary>
        static void ExecuteLongRunningTask()
        {
            Console.WriteLine("[긴 작업 스레드] 시작. 현재 상태: " + Thread.CurrentThread.ThreadState);
            for (int i = 0; i < 20; i++) // 충분히 긴 루프
            {
                Console.WriteLine($"[긴 작업 스레드] {i}번째 반복. 현재 초: {DateTime.Now.Second}");
                try
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }
                catch (ThreadAbortException)
                {
                    Console.WriteLine("[긴 작업 스레드] ThreadAbortException 감지됨. 작업 중단.");
                    // Thread.ResetAbort(); // Abort 취소
                    break;
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[긴 작업 스레드] 예외 발생: {ex.Message}");
                    break;
                }
            }
            Console.WriteLine("[긴 작업 스레드] 작업 완료.");
        }
    }
}

위 코드 실행 결과, quickTaskThread는 시작 후 Running 상태였다가 WaitSleepJoin 상태로 전환되고, 작업이 완료되면 Stopped 상태가 됩니다. longRunningThread는 유사한 패턴을 따르다 Abort() 호출 시 AbortRequested 상태가 될 수 있습니다. 이는 스레드가 중단될 준비가 되었음을 나타내지만, 실제 중단은 예외가 발생한 후에 이루어집니다. 이 예시는 스레드 생명 주기의 복잡성을 보여주며, 강제 스레드 종료가 스레드 상태를 예측하기 어렵게 만든다는 것을 시사합니다.

스레드 우선순위 (Priority)

스레드 우선순위는 스케줄러가 다음 실행할 스레드를 결정할 때 힌트를 제공합니다. ThreadPriority 열거형은 스레드의 우선순위를 Lowest, BelowNormal, Normal, AboveNormal, Highest로 설정할 수 있게 합니다. 이 우선순위는 특히 CPU 코어가 제한적이거나 스레드 경합이 심할 때 스레드의 실행 시간에 영향을 미칩니다.


using System;
using System.Threading;
using System.Diagnostics;

namespace ThreadPriorityApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"메인 스레드 시작. 현재 우선순위: {Thread.CurrentThread.Priority}");

            Console.WriteLine("\n--- 다중 코어 환경에서 스레드 우선순위 테스트 ---");
            DemonstratePriorities();
            Thread.Sleep(TimeSpan.FromSeconds(2)); // 이전 테스트 완료 대기 및 출력 기다림

            Console.WriteLine("\n--- 단일 코어 환경에서 스레드 우선순위 테스트 (ProcessorAffinity 설정) ---");
            // 현재 프로세스의 ProcessorAffinity를 단일 코어로 설정 (예: CPU 0번만 사용)
            // 이 설정은 프로세스 전체에 적용되며, 실제 운영 환경에서는 주의해서 사용해야 합니다.
            try
            {
                Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
                DemonstratePriorities();
            }
            catch (PlatformNotSupportedException)
            {
                Console.WriteLine("경고: ProcessorAffinity 설정이 현재 플랫폼에서 지원되지 않습니다.");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"ProcessorAffinity 설정 중 예외 발생: {ex.Message}");
            }


            Console.WriteLine("\n모든 우선순위 테스트 완료. 아무 키나 눌러 종료.");
            Console.ReadKey();
        }

        /// <summary>
        /// 다양한 우선순위를 가진 스레드들을 생성하고 실행하여 성능을 비교합니다.
        /// </summary>
        static void DemonstratePriorities()
        {
            var priorityTest = new PriorityWorker();

            Thread highPrioThread = new Thread(priorityTest.PerformHeavyCalculation) { Name = "HighPriorityWorker" };
            Thread lowPrioThread = new Thread(priorityTest.PerformHeavyCalculation) { Name = "LowPriorityWorker" };
            Thread normalPrioThread = new Thread(priorityTest.PerformHeavyCalculation) { Name = "NormalPriorityWorker" };

            // 스레드 우선순위 설정
            highPrioThread.Priority = ThreadPriority.Highest;
            normalPrioThread.Priority = ThreadPriority.Normal;
            lowPrioThread.Priority = ThreadPriority.Lowest;

            highPrioThread.Start();
            lowPrioThread.Start();
            normalPrioThread.Start();

            Thread.Sleep(2000); // 2초간 계산 실행
            priorityTest.StopWork(); // 모든 스레드에 작업 중단 요청
        }
    }

    /// <summary>
    /// 무거운 계산을 수행하며, 외부 요청에 의해 중단될 수 있는 작업자 클래스
    /// </summary>
    class PriorityWorker
    {
        private volatile bool shouldStop = false; // volatile 키워드 사용으로 가시성 보장

        public void StopWork()
        {
            shouldStop = true;
        }

        public void PerformHeavyCalculation()
        {
            long iterationCount = 0;
            while (!shouldStop)
            {
                iterationCount++;
            }
            Console.WriteLine($"[{Thread.CurrentThread.Name}] 우선순위: {Thread.CurrentThread.Priority,-10}, 총 계산 횟수: {iterationCount:N0}");
        }
    }
}

실행 결과를 보면, 다중 코어 환경에서는 스레드 우선순위의 영향이 크지 않습니다. 각 코어가 독립적으로 스레드를 실행할 수 있기 때문에 우선순위가 낮아도 충분한 실행 시간을 얻을 수 있습니다. 그러나 ProcessorAffinity를 사용하여 단일 코어 환경을 시뮬레이션하면, 우선순위가 높은 스레드가 훨씬 더 많은 CPU 시간을 할당받아 낮은 우선순위 스레드보다 월등히 많은 계산을 수행하는 것을 확인할 수 있습니다. 이는 스레드 스케줄링에서 우선순위가 어떻게 작용하는지를 명확히 보여줍니다.

전경 스레드와 배경 스레드

스레드는 기본적으로 전경(Foreground) 스레드로 생성됩니다. 전경 스레드는 프로세스가 살아있는 한 계속 실행되며, 모든 전경 스레드가 종료되어야 프로세스가 종료될 수 있습니다. 반면 배경(Background) 스레드는 프로세스에 남은 전경 스레드가 없을 경우, 자신의 작업이 완료되지 않았더라도 프로세스와 함께 강제로 종료됩니다. 이는 주로 백그라운드에서 로그 기록이나 캐시 정리와 같은 보조 작업을 수행할 때 유용합니다.


using System;
using System.Threading;

namespace ThreadTypeApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("메인 스레드 시작: 전경/배경 스레드 동작 비교");

            var fgWorker = new ThreadTypeDemonstrator(5, "전경 작업자"); // 5회 반복
            var bgWorker = new ThreadTypeDemonstrator(10, "배경 작업자"); // 10회 반복

            Thread fgThread = new Thread(fgWorker.RunCountingTask);
            fgThread.Name = "ForegroundThread";

            Thread bgThread = new Thread(bgWorker.RunCountingTask);
            bgThread.Name = "BackgroundThread";
            bgThread.IsBackground = true; // 이 스레드를 배경 스레드로 설정

            // 스레드 시작
            fgThread.Start();
            bgThread.Start();

            Console.WriteLine("메인 스레드 종료 대기 중...");
            // 메인 스레드는 기본적으로 전경 스레드이며,
            // 모든 전경 스레드가 종료될 때까지 프로세스 종료를 대기합니다.
            // fgThread가 완료되면 프로세스에 남아있는 전경 스레드가 없으므로,
            // 아직 실행 중인 bgThread는 강제로 종료되고 프로세스가 종료됩니다.

            Console.WriteLine("메인 스레드가 모든 전경 스레드 완료 후 종료됩니다.");
            // 여기서 메인 스레드는 바로 종료되므로, fgThread가 완료되면 프로세스가 종료된다.
            // bgThread는 fgThread가 완료되기 전에 종료되거나, fgThread 완료 시 강제 종료될 것이다.
        }
    }

    /// <summary>
    /// 숫자를 세는 작업을 수행하는 스레드 유형 데모 클래스
    /// </summary>
    class ThreadTypeDemonstrator
    {
        private readonly int maxCount;
        private readonly string workerName;

        public ThreadTypeDemonstrator(int countLimit, string name)
        {
            maxCount = countLimit;
            workerName = name;
        }

        public void RunCountingTask()
        {
            for (int i = 0; i < maxCount; i++)
            {
                Thread.Sleep(400); // 0.4초 대기
                Console.WriteLine($"[{workerName}] 스레드 '{Thread.CurrentThread.Name}' : {i + 1}번째 카운트");
            }
            Console.WriteLine($"[{workerName}] 스레드 '{Thread.CurrentThread.Name}' ({ (Thread.CurrentThread.IsBackground ? "배경" : "전경") } 스레드) 작업 완료!");
        }
    }
}

위 코드에서 전경 스레드는 5번 카운트하고, 배경 스레드는 10번 카운트하도록 설정했습니다. 하지만 프로그램을 실행하면, 전경 스레드가 5번 카운트를 완료하고 "ForegroundThread 작업 완료!" 메시지를 출력한 직후, 배경 스레드가 아직 10번 카운트를 다 채우지 못했더라도 프로세스는 강제로 종료됩니다. 이는 메인 스레드와 전경 스레드가 모두 종료되었기 때문에, 프로세스가 남아있는 배경 스레드를 자동으로 종료시키는 동작을 보여줍니다. 이러한 특성을 이해하면 애플리케이션의 종료 동작을 보다 정교하게 제어할 수 있습니다.

태그: C# .NET Multithreading threading ThreadState

7월 20일 20:00에 게시됨