스레드(Thread)

  • 15 minutes to read

닷넷에서 스레드(Thread)는 한 명의 작업자를 나타냅니다. 다중 스레드 또는 다중 스레딩은 여러 작업자를 두고 동시에 여러 작업을 처리하는 것을 말합니다. 이번 강의는 다중 스레드를 사용하여 병렬 프로그래밍하는 방법에 대해 알아봅니다.

> // 다중 스레딩: 동시에 여러 작업을 수행하여 앱의 응답성을 높이고, 다중 코어에서 처리량 향상
NOTE

스레드를 쉽게 기억하려면 'ㅅㄹㄷ'의 자음을 참고하여 "사람들"로 생각하실 수 있습니다.

스레드(Thread)

C#의 메인 메서드에서 실행되는 코드는 순차적으로 실행이 됩니다. 하지만, 메인 메서드에 또 다른 메서드 단위로 프로그램을 작성해 놓고 이를 스레드 개체를 통해서 실행하면 메서드의 실행 순서를 윈도? 운영 체제에게 맡길 수 있습니다. 그러면 순차적으로 실행되지 않고 반복적으로 여러 메서드를 나누어서 처리를 하게 됩니다. 스레드는 이처럼 순차적으로 처리되지 않고 여러 기능을 동시 다발적으로 실행하고자 할 때 사용하는 개념이며 이를 닷넷에서는 Thread와 같은 클래스로 제공됩니다.

프로세스와 스레드

그럼 먼저 프로세스와 스레드에 대해 알아봅시다.

  • 프로세스: 현재 실행 중인 프로그램을 프로세스라고 합니다.
  • 스레드: 운영 체제가 프로세서 시간을 할당하는 기본 단위입니다.

그림: 프로세스와 스레드

프로세스와 스레드

스레드는 한 명의 작업자 사람

스레드를 현실 세계에 비유를 들면 "한 명의 작업자 사람"을 의미합니다. 집에서 혼자 아침식사를 준비한다면 한 명의 사람(스레드)만 있어도 충분합니다. 하지만, 큰 식당에서는 여러 사람들(스레드)이 있어야 많은 양의 요리를 준비할 수 있습니다. 참고로, 여러 스레드를 사용하여 일을 진행하는 방식을 병렬(Parallel) 프로그래밍이라고 합니다.

매개 변수도 없고 반환값도 없는 메서드를 담을 대리자 사용

C#에서 스레드를 만들기 위해서 ThreadStart 대리자를 사용해야 합니다.

> public delegate void ThreadStart();

ThreadStart 대리자로 스레드 선언

스레드는 스레드에 담을 메서드를 여러 개 구현해 놓고 이를 ThreadStart 대리자에 등록하면 됩니다. ThreadStart 대리자 개체를 Thread 클래스의 생성자로 받은 후 Thread 개체의 Start() 메서드를 호출하여 스레드에 담긴 메서드를 호출하는 형태입니다.

> using System.Threading;
> 
> public static void Hi() { Console.WriteLine("Hi"); }
> 
> Thread t = new Thread(new System.Threading.ThreadStart(Hi));
> t.Start();
Hi

Thread 클래스의 주요 멤버

Thread 클래스는 다음과 같은 주요 속성 및 메서드를 제공합니다. 좀 더 자세한 내용은 Microsoft Learn 사이트를 참고하고, 아래 내용은 간단히 읽어보고 넘어갑니다.

  • Priority: 스레드의 우선순위를 결정합니다. ThreadPriority 열거형의 Highest, Normal, Lowest 값을 갖습니다.
  • Abort(): 스레드를 종료시킵니다.
  • Sleep(): 스레드를 설정된 밀리초(1000분의 1초)만큼 중지시킵니다.
  • Start(): 스레드를 시작합니다.

스레드 생성 및 호출

Thread 클래스와 ThreadStart 대리자를 사용하여 하나의 새로운 스레드를 만들고 이 스레드에 메서드를 담고 실행하는 내용을 코드로 살펴보겠습니다. 먼저 다음 코드를 작성 후 실행하세요.

코드: ThreadDemo.cs

using System;
using System.Threading;

/// <summary>
/// 하나의 스레드는 하나의 작업자
/// </summary>
class ThreadDemo
{
    static void Other()
    {
        Console.WriteLine("[?] 다른 작업자 일 실행");
        Thread.Sleep(1000); // 1초 대기(지연)
        Console.WriteLine("[?] 다른 작업자 일 종료");
    }

    static void Main()
    {
        Console.WriteLine("[1] 메인 작업자 일 시작");

        // `Thread` 클래스와 `ThreadStart` 대리자로 새로운 스레드 생성
        var other = new Thread(new ThreadStart(Other));
        other.Start(); // 새로운 스레드 실행

        Console.WriteLine("[2] 메인 작업자 일 종료");
    }
}
[1] 메인 작업자 일 시작
[2] 메인 작업자 일 종료
[?] 다른 작업자 일 실행
[?] 다른 작업자 일 종료

이번 코드의 의미는 메인 작업자와 다른 작업자의 두 사람이 일을 하는 것을 표현해 본 것입니다. 메인 작업자 스레드는 일을 시작하자마자 바로 실행되어 먼저 메시지가 출력되지만, 다른 작업자 스레드는 생성 후 1초간의 지연 시간을 발생시켜 나중에 Other() 메서드의 내용이 출력되는 것을 볼 수 있습니다. 코드 위치상으로는 [1]번과 [2]번 사이에 Other() 메서드 코드가 위치하지만, 스레드의 Start() 메서드를 호출할 때 새로운 스레드를 생성하고 실행하는 순간의 시간이 필요하기에 메인 작업자 스레드가 먼저 실행되는 형태로 출력되었습니다.

[실습] 다중 스레드를 사용한 메서드 함께 호출하기

3개의 메서드를 서로 다른 스레드 3개에 할당하여 실행하는 프로그램을 만들어 보겠습니다.

코드: ThreadPractice.cs

// 프로세스(Process): 하나의 프로그램 단위(프로젝트)
// 스레드(Thread): 프로세스안에서 실행하는 단위 프로그램(메서드)
using System;
using System.Diagnostics;
using System.Threading;

class ThreadPractice
{
    private static void Ide()
    {
        Thread.Sleep(3000); // 3초 딜레이 
        Console.WriteLine("[3] IDE: Visual Studio");
    }

    private static void Sql()
    {
        Thread.Sleep(3000); // 3초 딜레이 
        Console.WriteLine("[2] DBMS: SQL Server");
    }

    private static void Win()
    {
        Thread.Sleep(3000); // 3초 딜레이 
        Console.WriteLine("[1] OS: Windows Server");
    }

    static void Main()
    {
        //[1] 스레드
        ThreadStart ts1 = new ThreadStart(Win);
        ThreadStart ts2 = new ThreadStart(Sql);

        Thread t1 = new Thread(ts1);
        var t2 = new Thread(ts2);
        var t3 = new Thread(new ThreadStart(Ide))
        {
            Priority = ThreadPriority.Highest // 우선순위 높게 
        };

        t1.Start();
        t2.Start();
        t3.Start();

        //[2] 프로세스
        Process.Start("IExplore.exe"); // 익스플로러 실행
        Process.Start("Notepad.exe");  // 메모장 실행 
    }
}
[2] DBMS: SQL Server
[3] IDE: Visual Studio
[1] OS: Windows Server

[1]번 코드에서 스레드를 3개 생성하여 실행하면 실행 결과는 실행할 때마다 다르게 표현될 수 있습니다. 참고로, [2]번 코드 영역은 닷넷프레임워크 환경에서만 실행됩니다. Process 클래스의 Start() 메서드를 사용하면 윈도 운영 체제에서 익스플로러 및 메모장을 실행할 수 있습니다. 이 부분 코드는 실행이 안될 수도 있으니, 참고용으로 보셔도 좋습니다.

참고: 스레드 동기화(Synchronization)

여러 스레드를 동시에 실행할 때 발생할 수 있는 문제 중 하나는 하나의 스레드가 공유 리소스를 사용하는 동안 다른 스레드가 동일한 리소스에 접근하여 데이터 불일치나 오류가 발생하는 상황입니다. 이를 방지하기 위해 특정 코드 블록이나 리소스에 한 번에 하나의 스레드만 접근할 수 있도록 제어하는 방법을 스레드 동기화라고 합니다.

lock 문 사용

C#에서는 lock 문을 사용하여 간단하게 스레드 동기화를 구현할 수 있습니다. lock 문은 지정된 개체를 잠그고, 다른 스레드가 해당 개체를 잠그려고 시도할 경우 대기 상태에 놓이게 합니다.

private readonly object lockObject = new object();

public void CriticalSection()
{
    lock (lockObject)
    {
        // 스레드 동기화가 필요한 코드 블록
    }
}
  • lockObject는 반드시 참조형 개체이어야 하며, 일반적으로 private 액세스 한정자와 readonly 키워드가 적용된 개체를 사용합니다.
  • 잠금 범위는 최소화하여 성능 저하와 교착 상태를 방지해야 합니다.

자세한 내용은 Microsoft Learnlock 문서를 참고하세요.

병렬 프로그래밍

닷넷에는 TPL 이름의 병렬 라이브러리를 제공하기 때문에 병렬 프로그래밍을 쉽게 할 수 있습니다. C#의 병렬 프로그래밍도 큰 주제이므로 이번에는 동시성(Concurrency)과 병렬 처리(Parellel Processing)의 의미만을 간단히 살펴보겠습니다. 마찬가지로, 좀 더 자세한 내용은 Microsoft Learn 사이트를 참고하길 권장합니다.

동시성

우리가 지금까지 사용해 온 for 문은 동시성(Concurrency) 방식으로 순서대로 반복을 합니다. 다음 코드를 작성 후 실행하면 0부터 순서대로 값이 출력이 됩니다.

코드: ConcurrencyFor.cs

using System;

// 동시성(Concurrency)
class ConcurrencyFor
{
    static void Main()
    {
        for (int i = 0; i < 200000; i++)
        {
            Console.WriteLine(i);
        }
    }
}

다음 그림과 같이 순서대로 값이 실행되고 CPU 사용량을 보면 1개의 논리 프로세서만 100% 정도의 사용량을 보입니다.

그림: 하나의 프로세서만 열심히 일하기

하나의 프로세서만 열심히 일하기

병렬 처리

닷넷에서는 병렬 처리를 손쉽게 사용할 수 있는 API를 제공합니다. 다음의 Parallel 클래스의 For() 또는 ForEach()와 같은 메서드를 사용하면 병렬로 컴퓨터의 자원을 최대한 사용하여 빠르게 작업을 처리할 수 있습니다.

코드: ParallelFor.cs

using System;
using System.Threading.Tasks;

// 병렬 처리(Parallel Processing): 스레드를 직접 만들지 않고 다중 스레드로 처리
class ParallelFor
{
    static void Main()
    {
        Parallel.For(0, 200000, (i) => { Console.WriteLine(i); });
    }
}

다음 그림은 20만번의 반복을 진행하면서 값을 출력하는데, 순서대로 실행되지 않고 다중 스레드에 의해서 나눠서 실행됨을 엿볼 수 있습니다. 참고로 박용준 강사의 컴퓨터는 2개의 CPU를 사용하면서 44개의 논리 프로세서를 갖는 워크스테이션 PC다보니, 그림처럼 많은 코어가 100%로 열심히 일을 하고 있는 모습을 볼 수 있습니다. 병렬 처리는 동시성과 달리 컴퓨터의 자원을 최대한 사용하는데 이를 직접 코드로 구현하는 것보다는 이미 닷넷에서 제공하는 TPL 라이브러리를 살펴보면 좋습니다.

그림: 여러 개의 프로세서가 열심히 일하기

여러 개의 프로세서가 열심히 일하기

단순한 코드가 아닌 아주 오래 걸리는 다음 코드를 작성 후 .NET 9.0 기반에서 실행하니, 이번에는 모든 CPU가 100%를 사용하는걸 확인할 수 있습니다.

고부하 연산을 Parallel.For로 병렬 처리하기: ParallelForMore.cs

ParallelForMore.cs

Parallel.For(0, 200_000, (i) =>
{
    double result = 0;
    for (int j = 0; j < 100_000; j++) // 연산을 추가하여 CPU 부하 증가
    {
        result += Math.Sqrt(i) * Math.Pow(i, 2);
    }

    if (i % 50_000 == 0)
    {
        Console.WriteLine(
            $"i: {i}, Thread ID: {Thread.CurrentThread.ManagedThreadId}");
    }
});

프로세서 모두가 열심히 일하기

장 요약

다중 스레드와 병렬 프로그래밍에 대한 맛보기 예제 한 두개를 다뤄보았습니다. 스레드와 병렬 프로그래밍은 C# 고유의 문법이라기 보다는 닷넷에서 제공하는 클래스 라이브러리입니다. C#에 대한 이해를 높이고 게임 프로그래밍과 같은 현업 프로그램 작성시 성능적인 이슈가 발생할 때에 Microsoft Learn 사이트를 그때가서 찾아보셔도 되니, 지금은 이번 강의의 내용 정도만 맛보기로 살펴보고 다음 장으로 넘어가면 됩니다.

더 깊이 공부하고 싶다면
DevLec에서는 실무 중심의 C#, .NET, ASP.NET Core, Blazor, 데이터 액세스 강좌를 단계별로 제공합니다. 현재 수강 가능한 강좌 외에도 더 많은 과정이 준비되어 있습니다.
DevLec.com에서 자세한 커리큘럼을 확인해 보세요.
DevLec 공식 강의
C# Programming
C# 프로그래밍 입문
프로그래밍을 처음 시작하는 입문자를 위한 C# 기본기 완성 과정입니다.
ASP.NET Core 10.0
ASP.NET Core 10.0 시작하기 MVC Fundamentals Part 1 MVC Fundamentals Part 2
웹 애플리케이션의 구조와 MVC 패턴을 ASP.NET Core로 실습하며 익힐 수 있습니다.
Blazor Server
풀스택 웹개발자 과정 Part 1 풀스택 웹개발자 과정 Part 2 풀스택 웹개발자 과정 Part 3
실무에서 바로 활용 가능한 Blazor Server 기반 관리자·포털 프로젝트를 만들어 봅니다.
Data & APIs
Entity Framework Core 시작하기 ADO.NET Fundamentals Blazor Server Fundamentals Minimal APIs
데이터 액세스와 Web API를 함께 이해하면 실무 .NET 백엔드 개발에 큰 도움이 됩니다.
VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com