1. 도입
프로세스를 슈퍼마켓에 비유하면, 스레드(thread)는 서로 다른 계산대의 계산원들입니다. 계산원들이 동시에 손님을 처리하듯, 스레드도 같은 프로세스 안에서 병렬로 각자의 작업을 수행합니다. 스레드는 공통 자원을 공유하면서 하나의 애플리케이션 안에서 여러 작업을 동시에 수행하게 해줍니다. 핵심 장점은 프로세서가 멀티태스킹을 지원하면 스레드가 실제로 병렬로 동작할 수 있다는 점입니다.
실무에서 스레드가 필요한 경우
왜 여러 스레드를 띄워야 할까요? 현실에서 흔히 마주치는 몇 가지 상황:
- GUI 애플리케이션을 만들 때, 긴 작업 동안 UI가 "멈추지" 않게 하려면.
- 여러 파일을 동시에 다운로드해야 할 때.
- 게임에서 적들이 다른 것과 독립적으로 '생각'하게 만들고 싶을 때.
놀랍게도 많은 프로그램에서 스레드 사용 미숙으로 여전히 "멈춤"이 발생합니다. 오늘은 그런 실수를 피하는 법을 배웁니다.
클래스 Thread: 수동 멀티스레딩의 기초
클래스 Thread는 .NET 멀티스레딩에서 오래된 도구입니다. 더 현대적인 도구들(Task, async/await)이 있긴 하지만, Thread를 이해하면 스레드를 '맨땅에서' 다루는 감을 익히는 데 도움이 됩니다.
스레드 생성 단계
- 실행할 메서드를 전달해 Thread 객체를 생성한다.
- Start()로 스레드를 시작한다.
- (선택적) 무슨 일이 일어나는지 관찰한다 — 모든 것이 병렬로 돌아갈 수 있다!
2. Thread로 스레드 시작하기
이제 병렬의 마법을 직접 체감해봅시다. 조건부 애플리케이션에 어떤 수를 셈하고 진행 상황을 출력하는 작은 클래스를 추가해, 이를 별도 스레드에서 실행하도록 해보겠습니다.
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("메인 스레드가 시작되었습니다!");
// 실행할 메서드를 지정해 Thread 객체를 생성합니다
Thread workerThread = new Thread(CountToTen);
// 새 스레드를 시작합니다
workerThread.Start();
// 메인 스레드도 뭔가를 합니다: 점들을 찍고...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // 시각화를 위한 지연
}
Console.WriteLine("\n메인 스레드가 종료되었습니다!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[스레드] 계산 중: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[스레드] 완료!");
}
}
무슨 일이 일어나나요?
콘솔에서 점들과 "계산 중: X"가 뒤섞여 출력되는 걸 볼 수 있을 겁니다. 이게 멀티스레딩의 첫 신호예요! 메인 스레드는 점을 찍고, 새 스레드는 10까지 세고 있습니다. 서로 방해하지 않고, 마치 밴드의 두 연주자처럼 각자 소리를 냅니다.
3. 스레드에 데이터 전달하기
때로는 스레드가 무엇을 할지뿐 아니라 어떤 데이터로 작업할지도 알아야 합니다. 매개변수가 있는 메서드를 스레드에 전달하려면 어떻게 할까요?
방법 1: 람다(익명 메서드) 사용
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[스레드] {i} / {n}");
Thread.Sleep(300);
}
}
여기서는 필요한 메서드 호출을 람다로 감싸서 매개변수를 전달합니다. Thread는 매개변수 없는 메서드(ThreadStart)를 기대하기 때문에 이 방법이 많이 쓰입니다.
방법 2: ParameterizedThreadStart 사용
하나의 object 매개변수를 받는 특수 델리게이트 ParameterizedThreadStart를 사용할 수도 있습니다.
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[스레드] {i} / {max}");
Thread.Sleep(200);
}
}
매개변수 타입이 object라서 캐스팅이 필요합니다. 별로 깔끔하진 않지만 동작은 합니다. 요즘 C#에서는 람다 방식이 선호됩니다.
4. 스레드 생명주기 관리
이제 Thread가 제공하는 유용한 기능들을 살펴봅시다.
| 속성 / 메서드 | 용도 |
|---|---|
|
스레드(생성 시 지정한 메서드)를 시작합니다 |
|
스레드가 종료될 때까지 대기합니다(호출한 스레드를 블록) |
|
스레드가 현재 동작 중인지(true/false) 표시합니다 |
|
디버깅을 위해 스레드에 이름을 붙일 수 있습니다 |
|
현재 실행 중인 스레드를 나타내는 객체를 얻습니다 |
|
현재 스레드를 지정한 ms 밀리초 동안 멈춥니다 |
예제: 스레드 종료를 기다리기
때로는 메인 스레드가 보조 스레드의 작업이 끝날 때까지 기다려야 합니다.
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[두 번째 스레드] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[메인 스레드] 두 번째 스레드가 끝날 때까지 기다립니다...");
t.Join(); // 메인 스레드가 여기서 대기합니다
Console.WriteLine("[메인 스레드] 두 번째 스레드가 종료되었습니다!");
Join()가 없으면 프로그램이 보조 스레드가 여전히 실행 중인데도 종료될 수 있습니다. Join()을 쓰면 메인 스레드는 모든 작업이 끝날 때까지 참을성 있게 기다립니다.
5. 유용한 팁
스레드 이름 붙이기: 혼란을 줄이자
디버깅을 위해 스레드에 이름을 붙일 수 있습니다:
Thread t = new Thread(() =>
{
Console.WriteLine($"이 코드는 스레드에서 실행됩니다: {Thread.CurrentThread.Name}");
});
t.Name = "Counter-Thread";
t.Start();
스레드가 많아지면 누가 무슨 일을 하는지 파악하는 데 도움이 됩니다.
제한과 현실적인 미래
먼저 말하자면: 현대 애플리케이션에서는 수동으로 Thread를 직접 관리하는 일은 드뭅니다. 실제로는 더 강력하고 스마트한 도구들(Task, async/await)을 자주 사용합니다. 다만 스레드의 기초를 이해하면 다음에 도움이 됩니다:
- C#과 .NET 내부 동작을 이해하기 위해.
- 면접에서 Thread와 Task의 차이를 설명해야 할 때.
- 레거시 큰 애플리케이션에서 문제를 진단하고 해결할 때.
요약 다이어그램: 스레드 생명주기
stateDiagram-v2
[*] --> New: Thread 생성됨
New --> Running: Start()
Running --> Stopped: 메서드 종료
Stopped --> [*]
이제 여러분은 C#에서 스레드를 직접 생성하고 시작할 수 있습니다. 그냥 승객이 아니라 여러 차량을 동시에 운전하는 기관사 같은 존재가 된 셈이죠! 다음에는 생명주기 관리, 동기화, 병렬성의 새로운 지평을 다룹니다.
6. Thread 사용 시 흔한 실수와 요령
실수 #1: 스레드를 시작하지 않음.
Thread 객체를 생성하고도 Start()를 호출하는 것을 잊어버리는 경우가 많습니다. 그러면 스레드는 실행되지 않고 이유를 바로 찾기 어렵습니다.
실수 #2: 공유 데이터에 동기화 없이 접근함.
여러 스레드가 같은 변수를 보호 없이 수정하면 문제 발생 확률이 큽니다. 마치 두 계산원이 같은 현금함에서 거스름돈을 나눠줄 때 혼란이 나는 것과 같습니다.
실수 #3: 폐기되었거나 위험한 메서드 사용.
Thread.Suspend(), Thread.Resume() 같은 메서드는 위험하고 오래된 방식입니다. 스레드 생명주기는 다른 안전한 방식으로 관리하세요.
실수 #4: 스레드 내부의 예외를 처리하지 않음.
스레드에서 예외가 발생해 잡히지 않으면 그 스레드는 종료되고 메인 스레드는 이를 모를 수 있습니다! 스레드 코드를 try-catch로 감싸서 예외를 로그로 남기세요.
Thread t = new Thread(() =>
{
try
{
// ... 당신의 코드
}
catch (Exception ex)
{
// 에러를 로그로 남깁니다
Console.WriteLine($"[스레드] 오류: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION