스레드(Thread)

  • 14 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 라이브러리를 살펴보면 좋습니다.

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

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

장 요약

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

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com