스레드(Thread)
닷넷에서 스레드(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 Learn의 lock
문 문서를 참고하세요.
병렬 프로그래밍
닷넷에는 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 사이트를 그때가서 찾아보셔도 되니, 지금은 이번 강의의 내용 정도만 맛보기로 살펴보고 다음 장으로 넘어가면 됩니다.