1. 소개
스레드를 당신이 작업을 맡기는 지칠 줄 모르는 동료라고 상상해보세요. 동료는 잠들어 있을 수 있고(아직 일을 시작하지 않음), 땀 흘리며 일할 수 있고(당신의 메서드가 실행 중), 당신이 새 작업을 줄 때까지 기다릴 수 있고(대기), 또는 일을 마치고 퇴근할 수 있습니다(종료).
C#(그리고 전반적인 .NET)에서 스레드의 생명 주기는 몇 가지 상태로 구성됩니다:
- Unstarted — 스레드가 생성되었지만 아직 시작되지 않음.
- Running — 스레드가 실행 중.
- WaitSleepJoin — 스레드가 일시적으로 쉬고 있음(예: 신호를 기다리거나 "잠" 상태).
- Stopped — 스레드가 작업을 마치고 종료됨.
이 주기를 다음 다이어그램으로 직관적으로 표현할 수 있습니다:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: 신호 수신/시간 만료
Running --> Stopped: 메서드 종료
WaitSleepJoin --> Stopped: 메서드 종료
Stopped --> [*]
모든 것은 Thread 객체를 생성하는 것으로 시작하지만, Start()를 호출하기 전까지 스레드는 Unstarted 상태로 "눈을 붙이고" 있습니다. Start()를 호출하면 — 이제 본격적으로 실행되어 Running 상태가 됩니다. 코드 안에서 Thread.Sleep을 호출하거나 무언가를 기다리면 해당 스레드는 대기 상태로 들어갑니다. 전달한 메서드가 끝나면 스레드는 죽고 다시 "부활"하지 않습니다. 끝은 끝입니다.
2. 실습: 단순 스레드의 생명 주기
고전적인 예제를 살펴보겠습니다:
using System;
using System.Threading;
class Program
{
static void Main()
{
// 스레드를 생성 — 아직 작업만 계획함
Thread worker = new Thread(DoWork);
Console.WriteLine($"생성 후 스레드 상태: {worker.ThreadState}");
// 스레드 시작
worker.Start();
Console.WriteLine($"시작 후 스레드 상태: {worker.ThreadState}");
// 메인 스레드가 잠깐 쉬어서 워커 스레드가 일할 시간 제공
Thread.Sleep(100);
Console.WriteLine($"나중에 스레드 상태: {worker.ThreadState}");
// worker가 끝날 때까지 기다림 (조인)
worker.Join();
Console.WriteLine($"종료 후 스레드 상태: {worker.ThreadState}");
Console.WriteLine("메인 스레드 종료");
}
static void DoWork()
{
Console.WriteLine("워커 스레드가 작업을 시작했습니다!");
Thread.Sleep(500);
Console.WriteLine("워커 스레드가 작업을 마쳤습니다!");
}
}
프로그램이 출력하는 것?
- 스레드 생성 후 — 상태는 보통 Unstarted.
- 시작 후 — 보통 바로 Running (또는 Running | Background일 수 있음).
- 실행 중 — 상태는 Running 또는 스레드가 "잠" 상태면 WaitSleepJoin.
- 메서드 완료 후 — 상태는 Stopped.
이 코드는 스레드가 어떤 상태를 가질 수 있는지 이해하는 데 훌륭한 도구입니다. 지연 시간을 조절해서 상태 변화 관찰해보세요.
3. 스레드 제어: 주요 메서드
시작: Start()
명백하지만 다시 말하자면: 스레드를 만들면 Start()로 시작시켜야 합니다. 그리고 한 번만 시작할 수 있습니다 — Start()를 다시 호출하면 ThreadStateException이 발생합니다.
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // 오류!
종료 대기: Join()
때때로 스레드가 모두 끝날 때까지 기다렸다가 다음 작업을 해야 할 때가 있습니다. 그럴 때 Join()을 사용합니다.
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // t가 끝날 때까지 현재 스레드를 블록
여러 스레드가 있으면 각 스레드에 대해 Join()을 호출하면 메인 스레드는 모든 워커가 끝날 때까지 기다립니다.
옵션: Join(int millisecondsTimeout) 오버로드가 있어서 지정한 시간만큼만 기다리고 계속 진행할 수 있습니다.
// 최대 2초만 기다림
if (t.Join(2000))
Console.WriteLine("스레드가 제시간에 종료됨");
else
Console.WriteLine("기다리다 지쳤음...");
강제 종료: 왜 나쁜가
예전 .NET에는 스레드를 즉시 "죽이는" Thread.Abort() 같은 메서드가 있었습니다. 지금은 거의 사용하지 않죠 — 위험하고 프로그램을 이상한 상태로 남길 수 있습니다. .NET 철학은: 스레드는 자발적으로 종료되어야 한다. 동료를 강제로 내쫓지 말고, 퇴근 신호를 부드럽게 보내세요.
4. 스레드를 올바르게 "중지"하는 방법
스레드를 안전하게 중지하는 가장 좋은 방법은 스레드가 주기적으로 확인하는 취소 플래그(또는 완료 표시)를 사용하는 것입니다.
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("일하는 중!");
Thread.Sleep(300);
}
Console.WriteLine("명령으로 인해 스레드가 종료됩니다.");
}
public void RequestStop()
{
shouldStop = true;
}
}
사용 예:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// 잠깐 기다림
Thread.Sleep(1000);
// 스레드 종료 요청
w.RequestStop();
t.Join(); // 스레드 종료 대기
중요 포인트: volatile
volatile 키워드는 컴파일러와 프로세서에게 "이 필드를 캐시하지 말고 항상 최신 값을 읽어라!"라고 알려줍니다. 스레드가 최신 종료 신호를 보려면 중요합니다. 이것이 없으면(또는 다른 동기화 수단이 없으면) 스레드는 변경을 영원히 보지 못할 수 있습니다.
5. 스레드의 대기와 잠 상태로의 전환
때때로 스레드는 일시적으로 아무것도 하지 않습니다 — 기다리거나 잠들어 있습니다.
잠: Thread.Sleep
스레드에게 쉬게 하거나 실행 속도를 늦추고 싶을 때(예: CPU 과부하 방지), Thread.Sleep(milliseconds)을 사용합니다.
// 스레드가 2초 동안 잠듦
Thread.Sleep(2000);
잠자는 동안 스레드는 아무 작업도 수행하지 않습니다.
대기 / Join
메인 스레드가 자식 스레드가 끝나길 기다릴 때(Join) 메인 스레드는 "일시정지"됩니다. 마찬가지로 모니터나 다른 동기화 원시 타입으로 리소스 해제를 기다리면 해당 스레드는 대기 상태로 들어갑니다.
6. 스레드의 백그라운드/포어그라운드 제어
.NET에서는 스레드가 두 가지 종류가 있습니다: foreground(포어그라운드)와 background(백그라운드). 차이는 간단합니다:
- 프로세스에 백그라운드 스레드만 남아 있으면 프로세스는 자동으로 종료됩니다.
- 메인 스레드와 모든 포어그라운드 스레드가 종료되어야 프로세스가 멈춥니다.
스레드를 명시적으로 백그라운드로 설정할 수 있습니다:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // 백그라운드로 설정
t.Start();
실용 예 — 데몬 vs 일반 스레드
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("나는 팬텀(백그라운드), 멈추지 않아!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // 백그라운드로 만듦
t.Start();
Thread.Sleep(1200);
Console.WriteLine("메인 스레드가 종료됩니다");
// Main이 끝나면 프로세스가 종료되고 우리의 영원한 스레드도 사라짐
Main이 끝나면 프로세스가 종료되고, 백그라운드 스레드는 자동으로 중단됩니다.
7. 유용한 세부 사항
스레드에 대해 해서는 안 될 것들
- 스레드를 "재시작"할 수 없음. Thread 객체는 한 번만 사용: 메서드가 끝나면 스레드는 죽고 Start()를 다시 호출하면 예외가 발생합니다.
- 다른 스레드를 강제로 중단하지 말 것 — Thread.Abort()나 Thread.Suspend() 같은 메서드는 구식이고 위험합니다.
- 스레드 종료를 무시하지 마라. 파일이나 리소스를 다루는 스레드는 종료 전에 적절히 정리해야 합니다.
상태 확인과 생명 주기 제어
if (t.IsAlive)
{
Console.WriteLine("스레드가 아직 살아있음");
}
else
{
Console.WriteLine("스레드가 종료됨");
}
IsAlive는 스레드가 자신의 메서드를 실행하는 동안 true이고, 종료 후에는 false입니다.
.NET에서 단순 스레드의 생명 주기
| 상태 | 어떻게 도달 | 무슨 의미? | 어떻게 벗어남 |
|---|---|---|---|
| Unstarted | |
스레드가 생성되었지만 시작되지 않음 | Start() 호출 |
| Running | |
스레드가 작업 수행 중 | 메서드 종료 |
| WaitSleepJoin | Sleep(), Join(), 대기 | 스레드가 일시적으로 비활성 | 대기가 끝남 |
| Stopped | 스레드 메서드가 종료됨 | 스레드가 "사망" | 다시 나오지 않음 — 끝 |
스레드 생명 주기 관리 FAQ
질문: 명령으로 스레드를 죽일 수 있나?
답변: 아니오, 그리고 그럴 필요도 없습니다. 스레드는 스스로 종료를 감지하도록 해야 합니다. 취소 플래그를 사용하세요.
질문: Thread 객체를 재사용할 수 있나?
답변: 아니오. 새 작업을 위해 새 객체를 만드세요.
질문: 메인 스레드가 종료되었는데 자식 스레드가 계속 일하면 어떻게 되나?
답변: 자식 스레드가 백그라운드(IsBackground == true)면 애플리케이션이 종료됩니다. 아니면 모든 스레드가 끝날 때까지 프로세스는 계속 실행됩니다.
질문: 스레드가 취소로 종료될 때 자원 정리는 어떻게 하나?
답변: 스레드 메서드 안에서 try...finally 블록을 사용해 어떤 경우에도 자원이 해제되게 하세요.
8. 스레드 작업 시 흔한 실수와 방지 방법
실수 #1: 동일한 Thread 객체를 재사용하려 함.
하나의 Thread 객체를 두 번 이상 시작할 수 없습니다. 스레드가 끝나면 재시작 시도는 예외를 일으킵니다.
실수 #2: 스레드 내에서 외부 자원 해제 실패.
스레드가 파일, 네트워크, 기타 자원을 사용하면 반드시 올바르게 닫고 해제해야 합니다. finally 블록이나 using 구문을 사용해 누수와 데드락을 피하세요.
실수 #3: 너무 많은 스레드 생성.
스레드를 과도하게 생성하면 디버깅이 복잡해지고 성능이 저하될 수 있습니다. 불필요한 스레드는 문제를 만들 뿐입니다.
GO TO FULL VERSION