스레드(Thread)와 스레드풀(ThreadPool)

황현중·2025년 12월 1일

1. 스레드(Thread)가 뭐야?

1-1. 회사/직원 비유로 이해하기

먼저 비유부터 해 보자.

  • 프로세스(Process) = 회사
  • 스레드(Thread) = 회사 안에서 일하는 직원들

어떤 프로그램(EXE)을 실행하면, 운영체제 입장에서는 프로세스 하나가 생긴다.
그리고 그 안에서 실제로 일을 하는 단위가 스레드다.

예를 들어 엑셀을 켜면:

  • 엑셀 프로세스 1개
  • 그 안에
    • 화면 그리는 스레드
    • 마우스/키보드 입력 받는 스레드
    • 자동 저장하는 스레드
    같이 여러 스레드가 동시에 일을 할 수 있다.

C# 콘솔 프로그램도 마찬가지다.

  • 프로그램이 실행되면 Main 스레드 하나로 시작한다.
  • 우리가 필요하다면 Thread를 만들어 스레드를 더 늘릴 수 있다.

2. C#에서 Thread 직접 만들어 보기

가장 기초적인 예제부터 보자.

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("메인 스레드 시작");

        // 1) 새 스레드에서 실행할 작업 정의
        Thread t = new Thread(DoWork);

        // 2) 새 스레드 시작
        t.Start();

        // 3) 메인 스레드는 자기 할 일 계속
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine($"[Main] i = {i}");
            Thread.Sleep(500); // 0.5초 쉬기
        }

        // 4) 새 스레드가 끝날 때까지 기다리고 싶으면 Join 사용
        t.Join();

        Console.WriteLine("메인 스레드 종료");
    }

    static void DoWork()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine($"[Worker] i = {i}");
            Thread.Sleep(700); // 0.7초 쉬기
        }
    }
}

2-1. 이 코드에서 일어나는 일

  • 프로그램 시작 → Main 스레드 시작
  • new Thread(DoWork) : 새 직원(스레드)을 한 명 더 채용
  • t.Start() : 그 직원에게 DoWork 일을 시킴
  • Main 스레드는 for 루프를 돌면서 [Main] ... 출력
  • 새 스레드는 DoWork 안에서 [Worker] ... 출력

콘솔에 [Main] ..., [Worker] ...가 섞여서 출력되면
두 개의 스레드가 동시에 일을 하고 있다는 뜻이다.

  • Thread.Sleep(500) : “이 스레드는 0.5초 동안 잠깐 쉰다”
  • t.Join() : “스레드 t가 끝날 때까지 여기서 기다린다”

입문 단계에서는 “스레드를 직접 만들면 이렇게 여러 작업을 동시에 돌릴 수 있다” 정도만 이해하고 넘어가면 된다.


3. 스레드풀(ThreadPool)은 뭐야?

3-1. 비유로 먼저 이해하기

방금 본 new Thread(...)는 이렇게 생각할 수 있다.

“작업 하나 처리하려고, 직원을 한 명 새로 채용한다.”

그런데 현실에서 이러면 비효율적이다.

  • 작업이 조금 생길 때마다 사람 채용 → 작업 끝나면 바로 해고
  • 채용/해고 비용이 너무 크다

그래서 회사에서는 보통 이렇게 한다.

“기본 직원들을 몇 명 쭉 고용해 두고, 일이 생기면 그 직원들에게 나눠준다.”

이게 바로 스레드풀(Thread Pool) 개념이다.

  • 미리 만들어 놓은 스레드들의 “풀(pool)”이 있고
  • 우리는 “이 작업 좀 처리해줘”라고 일(작업)만 던져준다.
  • 풀 안에서 놀고 있는 스레드가 그 일을 가져다가 실행한다.

.NET 내부에는 이미 ThreadPool이 준비되어 있고, 우리는 거기에 작업을 큐(queue)에 넣어 사용하면 된다.


4. ThreadPool 기본 사용 예제

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("메인 스레드 시작");

        // 스레드풀에 작업을 큐에 넣기
        ThreadPool.QueueUserWorkItem(DoWork);

        Console.WriteLine("메인 스레드는 계속 진행 중...");
        Console.ReadKey(); // 프로그램이 바로 종료되지 않게 대기
    }

    // 스레드풀용 콜백 메서드는 보통 object? 하나를 인수로 받는 시그니처를 가진다.
    static void DoWork(object? state)
    {
        Console.WriteLine(
            $"스레드풀 작업 시작, Thread ID = {Thread.CurrentThread.ManagedThreadId}");

        Thread.Sleep(1000); // 1초 정도 일하는 척
        Console.WriteLine("스레드풀 작업 끝");
    }
}

4-1. 포인트 정리

  • ThreadPool.QueueUserWorkItem(DoWork);
    • “스레드풀에 이 메서드 실행할 작업 하나 넣어줘”라는 의미
    • 새 스레드를 직접 만드는 것이 아니라, 풀에 있는 스레드를 재사용한다.
  • DoWork(object? state)
    • ThreadPool에서 호출할 메서드는 인자로 object? 하나 받는 형태가 기본
    • 지금 예제에서는 state를 사용하지 않으니까 null 그대로 둬도 된다.

5. Thread 직접 생성 vs ThreadPool 비교

구분 new Thread 직접 생성 ThreadPool / Task.Run
스레드 생성 비용 스레드 만들 때마다 비용 큼 미리 만든 스레드를 재사용 → 효율적
수명 관리 Start, Join, 종료 시점 직접 관리 작업만 던지면 끝나면 알아서 반환
개수 제한 관리 너무 많이 만들면 OS에 부담 .NET이 내부적으로 최대 스레드 수를 조절
사용 난이도 입문자에게는 다소 복잡 Task.Run, async/await로 비교적 쉬움
요즘 권장 방식 특수한 경우 아니면 잘 안 씀 대부분 스레드풀 + Task / async 조합 사용

실무에서는 보통:

직접 Thread를 만드는 경우는 특수한 상황이고,
대부분은 스레드풀 + Task / async/await 조합으로 처리한다.

6. 사실 Task.Run도 스레드풀 위에서 돈다

요즘 C# 코드에서는 ThreadPool.QueueUserWorkItem 대신 Task.Run을 훨씬 더 자주 쓴다.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("메인 시작");

        // Task.Run 안의 코드는 스레드풀 스레드에서 실행된다.
        await Task.Run(() =>
        {
            Console.WriteLine(
                $"백그라운드 작업, Thread ID = {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Console.WriteLine("백그라운드 작업 끝");
        });

        Console.WriteLine("메인 종료");
    }
}

6-1. 여기서 알아둘 점

  • Task.Run(() => { ... })는 내부적으로 스레드풀에 작업을 던지는 것이라고 생각하면 된다.
  • 우리는 “이 코드를 백그라운드에서 돌려줘” 정도의 느낌만 가지고 사용해도 된다.
  • await를 쓰면, 그 작업이 끝난 다음에 다시 이어서 실행할 수 있다.

그래서 요약하면:

  • 과거: new Thread 또는 ThreadPool.QueueUserWorkItem 사용
  • 현재: 대부분 Task + async/await 사용 (내부에서는 결국 스레드풀이 일을 한다)

7. 실전 느낌으로 보는 간단 예제

화면(UI)을 가진 프로그램(윈폼, WPF 등)에서는
무거운 작업을 메인(UI) 스레드에서 돌리면 화면이 멈춘다.
그래서 보통 스레드풀/백그라운드에서 작업을 돌리고, 결과만 가져온다.

콘솔에서 비슷한 느낌만 보이는 예제를 하나 보자.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        Console.WriteLine("프로그램 시작");

        // 무거운 작업을 스레드풀에서 실행
        Task<long> work = Task.Run(() =>
        {
            Console.WriteLine("무거운 작업 시작");
            Thread.Sleep(3000); // 3초 걸리는 작업이라고 가정
            Console.WriteLine("무거운 작업 끝");
            return 12345L; // 결과 예시
        });

        Console.WriteLine("메인은 다른 일 계속 할 수 있음...");

        // 여기서 결과를 기다림 (중간에 UI라면 다른 이벤트 처리 가능)
        long result = await work;

        Console.WriteLine($"작업 결과: {result}");
        Console.WriteLine("프로그램 종료");
    }
}

7-1. 실행 흐름

  1. 프로그램 시작 출력
  2. 무거운 작업 시작 출력 (스레드풀 스레드)
  3. 메인은 다른 일 계속 할 수 있음... 출력 (메인 스레드)
  4. 3초 후 무거운 작업 끝 출력
  5. await work 이후 작업 결과: 12345 출력
  6. 프로그램 종료 출력

중요한 건, 무거운 작업 때문에 메인 흐름이 멈추지 않는다는 점이다.
이런 패턴이 UI 프로그램이나 서버 프로그램에서 매우 자주 사용된다.


8. 입문자가 꼭 기억하면 좋은 포인트

  • 스레드(Thread)
    • 프로그램 안에서 실제로 일을 하는 실행 흐름
    • C# 프로그램은 기본적으로 Main 스레드 1개로 시작
    • new Thread(...) 로 직접 스레드를 만들 수 있지만, 요즘은 특별한 경우에만 사용
  • 스레드풀(ThreadPool)
    • .NET이 미리 만들어 관리하는 “스레드 직원 풀”
    • ThreadPool.QueueUserWorkItem 또는 Task.Run으로 작업을 던지면, 풀 안의 스레드가 실행
    • 스레드를 직접 만드는 것보다 효율적이고 안전한 경우가 많다.
  • Task / async / await
    • 스레드/스레드풀을 좀 더 쉽게 쓰도록 도와주는 고수준 도구
    • 내부적으로는 스레드풀이나 비동기 I/O를 활용
    • 입문자는 “비동기 코드는 Task + async/await로 작성한다”부터 익숙해지는 것이 좋다.
  • UI 프로그램에서는
    • 무거운 작업을 메인(UI) 스레드에서 직접 돌리면 화면이 멈춘다.
    • 스레드풀/백그라운드에서 일을 돌리고, 결과만 UI 스레드로 가져오는 패턴을 사용한다.

9. 이후 찾아보면 좋은 부분

스레드와 스레드풀 개념을 이해했다면, 다음 단계로 이런 것들을 공부해 보면 좋다.

  • 동기화 : lock, Monitor, Interlocked
  • 스레드 안전(Thread-Safety) 개념
  • async/await가 스레드를 새로 만드는 게 아니라는 점 (I/O 비동기 개념)

0개의 댓글