1. 도입
스레드를 아이스크림 줄에 선 사람들로 생각해보자. 어떤 사람은 조용히 기다리고, 어떤 사람은 소란을 피워서 ("나 곧 기차 타야 해, 먼저 좀 보내줘!") 가게 주인이 모두의 말을 듣는 것 같지만 가끔은 줄을 건너뛰어 누구를 먼저 응대하기도 한다 — 예를 들어 우는 아이를 안은 엄마 같은 경우. 스레드 우선순위도 그런 느낌이다.
C# (정확히는 .NET)에서 각 스레드에는 우선순위가 있는데, 이는 운영체제에 해당 스레드가 다른 스레드보다 얼마나 중요한지에 대한 힌트다. 이건 OS에 대한 엄격한 규칙은 아니다 (가장 우선순위가 높은 스레드가 모두의 관심을 항상 받는다는 보장은 없다), 하지만 보통 우선순위가 높은 스레드는 더 많은 프로세서 시간을 받는다.
이게 실제로 필요한 경우
- 사용자 인터페이스: 예를 들어 화면 업데이트는 낮은 우선순위의 긴 계산으로 지연되면 안 된다.
- 게임: 프레임 렌더링은 백그라운드에서 맵을 생성하는 것보다 더 중요하다.
- 반응 시간이 중요한 작업: 장비 제어, 신호 처리 등.
2. 스레드 우선순위: 어떻게 동작하는가
C#에서 스레드 우선순위를 다루는 건 간단하다 — Thread 객체에 Priority 속성이 있다. 이 속성은 ThreadPriority 열거형의 값을 받는다:
| 값 | 설명 |
|---|---|
|
가장 낮은 우선순위 |
|
보통보다 낮음 |
|
보통 (기본값) |
|
보통보다 높음 |
|
가장 높은 우선순위 |
코드 예시:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread lowPriorityThread = new Thread(PrintLowPriority);
Thread highPriorityThread = new Thread(PrintHighPriority);
lowPriorityThread.Priority = ThreadPriority.Lowest;
highPriorityThread.Priority = ThreadPriority.Highest;
lowPriorityThread.Start();
highPriorityThread.Start();
}
static void PrintLowPriority()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("낮은 우선순위: " + i);
Thread.Sleep(10); // 차이를 보기 위해 잠시 멈춤
}
}
static void PrintHighPriority()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("높은 우선순위: " + i);
Thread.Sleep(10);
}
}
}
참고
실무에서는 특히 현대의 멀티코어 시스템과 관리되는 .NET 환경에서 우선순위는 운영체제에 대한 권고일 뿐이다. OS(및 .NET)는 노력한다—우선순위가 높은 스레드에 더 많은 시간을 할당하려고 하지만, 확실한 보장은 없다. 예를 들어, 어떤 스레드가 Highest로 CPU를 독점하면 UI가 버벅거리고 다른 스레드들이 고통받을 수 있다.
3. 스레드 우선순위 변경
스레드와 우선순위
graph LR
A[스레드 1 - Lowest] -->|더 적은 CPU 시간| OS(운영체제)
B[스레드 2 - Normal] --> OS
C[스레드 3 - Highest] -->|더 많은 CPU 시간| OS
OS --> CPU(프로세서)
왜 스레드 우선순위를 바꿀까?
항상 Highest로만 해두면 된다고 생각할 수도 있지만, 그건 나쁜 생각이다! 가게에 있는 모든 손님이 동시에 "먼저 좀 봐주세요!"라고 외친다고 생각해보라.
우선순위는 정당한 이유가 있을 때만 바꿔라:
- 로그를 쓰는 백그라운드 스레드는 BelowNormal이나 Lowest로 해도 된다.
- 사용자 입력을 처리하는 스레드는 AboveNormal이 적절할 수 있다.
- CPU 집약적인 작업 중 시간에 민감하지 않은 작업은 낮은 우선순위를 준다.
팁: 애플리케이션이 느려지면, 무작정 높은 우선순위를 가진 "버릇없는" 스레드가 있는지 확인해봐라!
4. .NET의 스레드 유형
C#에서는 전통적으로 두 가지 타입의 스레드를 구분한다: foreground와 background. 놀랍게도 "background"가 항상 우리가 생각하는 '뒤에서 실행되는 것'과 정확히 같지는 않다. 아래에서 정리한다.
Foreground 스레드 (주요)
- 기본적으로 여러분이 생성하는 스레드는 모두 foreground다.
- 프로세스에 foreground 스레드가 하나라도 살아 있는 한, 애플리케이션은 종료되지 않는다.
- 예: 프로그램의 메인 스레드(Main)와 new Thread()로 생성된 기본 스레드들.
Background 스레드 (보조)
- 모든 foreground 스레드가 종료되고 background 스레드만 남아있으면 프로세스는 바로 종료되고 background 스레드는 예고 없이 중단된다.
- 로깅, 메트릭 전송 같은 보조 작업에 적절하다.
- 스레드를 background로 만들려면 IsBackground를 true로 설정한다:
Thread t = new Thread(SomeMethod);
t.IsBackground = true;
t.Start();
차이점 데모
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread backgroundThread = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Background thread 동작 중... " + i);
Thread.Sleep(500);
}
Console.WriteLine("Background 스레드가 종료되었습니다.");
});
backgroundThread.IsBackground = true; // 스레드를 백그라운드로 설정!
backgroundThread.Start();
Console.WriteLine("Main은 1초 후에 종료합니다.");
Thread.Sleep(1000); // 1초 기다림
Console.WriteLine("Main이 종료되었습니다. Background 스레드는 어떻게 될까?");
}
}
무엇을 보게 될까?
Main이 종료되면 background 스레드는 도중에 "강제 종료"될 수 있다. 이 동작은 애플리케이션 종료를 방해하면 안 되는 보조 작업에 유용하다.
5. 스레드 종류
ThreadPool (스레드 풀)
- ThreadPool은 너무 많은 스레드를 매번 생성하지 않도록 관리해주는 메커니즘이다.
- 작고 짧은 작업이 많을 때 주로 사용한다: 병렬 요청 처리, 비동기 작업 등.
- 풀에서 가져온 스레드는 항상 background이다 — 애플리케이션 종료를 막지 않는다.
예시: 스레드 풀에서 작업 실행
using System;
using System.Threading;
class Program
{
static void Main()
{
ThreadPool.QueueUserWorkItem(DoWorkInThreadPool, "백그라운드 작업");
Console.WriteLine("Main이 종료됩니다.");
Thread.Sleep(500);
}
static void DoWorkInThreadPool(object? state)
{
Console.WriteLine("풀에서 온 스레드: " + state);
Thread.Sleep(1000); // 잠깐 쉬어보자
Console.WriteLine("풀에서 온 스레드가 종료되었습니다!");
}
}
주의할 점:
만약 Main이 더 빨리 종료되면, 풀에서 실행 중이던 스레드는 중간에 끊길 수 있다. 완료 보장이 필요하면 foreground 스레드를 사용하거나 명시적으로 완료를 기다려라.
멀티스레딩의 진화: Task, async/await
요즘 애플리케이션에서는 수동으로 Thread를 생성하는 경우가 드물고, 보통은 Task와 비동기 메서드를 사용한다. 알아둘 점:
- 풀의 스레드와 Task는 background 스레드에서 동작한다.
- 대부분의 경우 우선순위를 굳이 바꿀 필요는 없다 — 상식과 모범 사례를 따르자.
6. 유용한 팁
스레드 속성과 동작
| 속성 | Foreground Thread | Background Thread | ThreadPool Thread |
|---|---|---|---|
|
기본적으로 false | true | true |
| 애플리케이션 종료 | 최소 하나의 foreground 스레드가 살아 있으면 애플리케이션이 종료되지 않음 | 모든 foreground 스레드가 종료되면 애플리케이션이 종료됨 | Main이 종료되면 애플리케이션이 종료됨 |
| 제어 | 완전한 제어 | 완전한 제어 | 직접 제어 불가 |
| 사용 용도 | 장기간 실행되거나 중요한 작업(예: DB 서버) | 비핵심 작업, 백그라운드 작업, 로깅 | 짧은 작업, Task, 비동기 작업 |
| 우선순위 | 설정 가능 | 설정 가능 | 변경 권장하지 않음 |
눈에 잘 띄지 않는 팁 및 주의사항
- 우선순위는 일반적인 Thread에만 설정할 수 있고, 풀에서 가져온 작업(Task, ThreadPool)에는 적용할 수 없다.
- 풀의 스레드는 항상 Normal 우선순위를 가지며 변경할 수 없다.
- Task와 async/await는 우선순위와 백그라운드 실행을 내부에서 처리해주므로 보통은 신경 쓸 일이 적다.
7. 우선순위와 스레드 유형에서 흔한 실수들
실수 №1: 우선순위 남용.
별 이유 없이 모든 스레드를 Highest로 만들거나 반대로 모두 Lowest로 설정하면 도움이 되지 않는다. 실행 균형을 깨고 애플리케이션의 반응성을 해칠 수 있다.
실수 №2: background 스레드의 암묵적 종료.
중요한 작업(예: 데이터 저장)을 background 스레드에서 하고 끝날 때까지 기다리지 않으면 데이터가 손실될 위험이 있다. background 스레드는 프로세스 종료 시 자동으로 중단된다.
실수 №3: 우선순위에 대한 과도한 기대.
스레드의 Priority는 운영체제에 대한 권고일 뿐이며, 그 스레드가 항상 먼저 실행된다는 보장은 아니다.
GO TO FULL VERSION