스레드 중단 (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번 카운트를 다 채우지 못했더라도 프로세스는 강제로 종료됩니다. 이는 메인 스레드와 전경 스레드가 모두 종료되었기 때문에, 프로세스가 남아있는 배경 스레드를 자동으로 종료시키는 동작을 보여줍니다. 이러한 특성을 이해하면 애플리케이션의 종료 동작을 보다 정교하게 제어할 수 있습니다.