티스토리 뷰
반응성 좋은 프로그램을 만들려면 병렬/비동기 프로그래밍은 거의 필수입니다.
병렬 프로그래밍은 작업이 오래 걸리는 부분을 여러 곳에서 동시에 처리하는 것이고
비동기 프로그래밍은 작업이 오래 걸리는 부분은 일단 다른 곳에서 처리하도록 해서 현재의 흐름을 방해하지 않고 진행하다가 처리가 끝나면 다시 돌아와서 처리하는 것을 뜻합니다.
어찌됐든 병렬/비동기 프로그래밍 모두 Thread와 밀접한 연관이 있습니다.
따라서 이번 시간에서는 C#에서 Thread를 사용하는 여러 가지 방법에 대해 알아보겠습니다.
C#에서 Thread를 사용하는 방법은 여러 가지 있습니다.
오늘 소개할 방법은 총 3가지입니다.
각각은 Thread와 ThreadPool 그리고 Task입니다.
각각에 대해서 깊게 살펴보지는 않을 것이고 각각의 사용법과 차이점에 대해서 알아볼 예정입니다.
가장 먼저 Thread입니다.
Thread는 System.Threading이라는 namespace에 정의되어 있으며 .NET Framework 1.0부터 사용 가능합니다.
Thread에 실행될 로직을 넘겨주며 생성한 후 실행시키면 새로 Thread를 만들어서 해당 로직을 실행시켜줍니다.
다음으로는 ThreadPool입니다.
역시 System.Threading이라는 namespace에 정의되어 있으며 .NET Framework 1.0부터 사용 가능합니다.
ThreadPool은 Thread를 효율적으로 사용할 수 있도록 하기 위해 만들어진 class입니다.
Thread는 생성, 삭제될 때 아무래도 많은 시스템 자원을 사용합니다.
따라서 미리 생성해두고 필요할 때마다 사용하는 pooling 기법을 통해 Thread를 사용하는 방식을 제공한 것입니다.
마지막으로는 Task입니다.
Task는 앞의 Thread, ThreadPool과는 다르게 System.Threading.Task라는 namespace에 정의되어 있으며 .NET Framework 4.0부터 사용 가능합니다.
Task는 내부적으로는 ThreadPool을 사용한다고 하며 기존 ThreadPool의 여러 가지 단점을 보완한 것입니다.
그리고 .NET 5.0에 추가된 async, await라는 키워드와 잘 호환된다고 합니다.
Task는 직접 생성해서 사용하거나 Task.Run 함수 혹은 TaskFactory.StartNew를 통해서 호출할 수 있습니다.
이로써 Thread, ThreadPool, Task에 관해서 간략하게 알아봤습니다.
다음으로는 각 class를 사용하는 방법에 대해서 알아보겠습니다.
System.Threading.Thread
가장 먼저 Thread의 사용법을 알아봅시다.
-
public static void Main(string[] args)
-
{
-
UsingThread();
-
Console.WriteLine("End of Main");
-
}
-
-
private static void UsingThread()
-
{
-
Thread threadWithoutParameter = new Thread(() => Console.WriteLine("thread1"));
-
threadWithoutParameter.Start();
-
-
Thread threadWithParameter = new Thread((num) => Console.WriteLine("thread2 " + num));
-
threadWithParameter.Start(22);
-
}
이렇게 사용하며 결과는 아래와 같습니다.

경우에 따라 쓰레드 실행 순서가 다르므로 출력되는 내용의 순서는 다를 수 있습니다.
위 예제에서는 new Thread를 통해 쓰레드를 생성하고 Start를 통해 쓰레드를 실행시키고 있습니다.
Thread 생성자는 각각 ThreadStart와 ParameterizedThreadStart delegate를 인자로 받는 함수를 오버로딩하고 있습니다.
ThreadStart는 void를 리턴하고 인자를 받지 않는 함수 타입이고
ParameterizedThreadStart는 void를 리턴하고 object 타입의 인자를 받는 함수 타입입니다.
Thread의 Start 함수 역시 인자를 받지 않는 함수와 object?를 인자로 받는 함수를 오버로딩하고 있습니다.
따라서 인자를 받지 않는 쓰레드를 생성했다면 Start를 호출할 때 인자를 보내지 않아야 하고 인자를 받는 쓰레드를 생성했다면 Start를 호출할 때 인자를 보내주면 됩니다.
인자를 받지 않는 쓰레드를 생성하고 Start를 호출할 때 인자를 보낼 경우 예외가 발생합니다.
System.Threading.ThreadPool
다음으로는 ThreadPool의 사용법을 알아봅시다.
-
private static void UsingThreadPool()
-
{
-
ThreadPool.QueueUserWorkItem((_) => Console.WriteLine("queue user work itme"));
-
ThreadPool.QueueUserWorkItem((num) => Console.WriteLine("queue user work item " + num), 22);
-
}
Thread를 사용할 때보다는 조금 더 간편하게 사용할 수 있습니다.
실행할 함수를 인자로 해서 static 함수인 QueueUserWorkItem를 호출하면 자동으로 쓰레드 풀의 쓰레드로 실행됩니다.
쓰레드를 직접 생성하는 과정이 없기 때문에 사용법도 간단하고 추가적인 오버헤드도 없습니다.
하지만 간편한 대신 커스터마이징하려면 조금 어려운 점이 있습니다.
예를 들어 실행한 쓰레드가 끝날때까지 기다리던가 혹은 결과값을 받아오거나 하는 것이 어렵습니다.
물론 EventWaitHandle이라는 것을 사용하면 가능하기는 하지만 쉽지 않고 코드를 짜더라도 한 눈에 파악하기 어렵습니다. 간단한 예제를 보자면 아래와 같이 작성하면 쓰레드가 끝나기까지 대기한 후 결과값을 받아올 수 있습니다.
-
private static void UsingThreadPoolEventWaitHandler()
-
{
-
Hashtable hashtable = new Hashtable();
-
EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset);
-
hashtable["data"] = 1;
-
hashtable["eventwaithandle"] = ewh;
-
-
ThreadPool.QueueUserWorkItem(ThreadPoolThreadFunc, hashtable);
-
-
// 인자로 보낸 EventWaitHandle이 set 될 때까지 대기
-
ewh.WaitOne();
-
Console.WriteLine("result: " + hashtable["data"]);
-
}
-
-
private static void ThreadPoolThreadFunc(object inst)
-
{
-
Hashtable hashtable = inst as Hashtable;
-
-
int data = (int)hashtable["data"];
-
-
// 인자로 받은 data를 처리하고 EventWaitHandle을 Set
-
data += 5;
-
hashtable["data"] = data;
-
(hashtable["eventwaithandle"] as EventWaitHandle).Set();
-
}
위 예제를 보시면 QueueUserWorkItem에서 실행할 쓰레드에 인자를 보낼 때 여러 개를 보낼 수 없기 때문에 Thread로 실행한 함수에 인자로 Hashtable을 보냈습니다.
Hashtable에는 실질적인 데이터와 쓰레드 실행 완료를 파악하기 위해 EventWaitHandle을 담았습니다.
Thread를 호출하는 쪽인 UsingThreadPoolEventWaitHandler 함수에서는 WaitOne을 이용해서 Thread의 실행 완료를 대기하고 있고 Thread로 실행되는 쪽인 ThreadPoolThreadFunc 함수에서는 마지막에 Set 함으로써 실행 완료했음을 알렸습니다.
아무튼 딱 봐도 아시겠지만 흐름이 한 눈에 들어오지는 않습니다.
따라서 이러한 단점을 보완한 것이 바로 다음에 설명할 Task입니다.
System.Threading.Tasks.Task
-
private static void UsingTask()
-
{
-
// num을 인자로 받고 리턴값이 없는 task를 생성
-
Task task1 = new Task((num) => Console.WriteLine("task action " + num), 2);
-
task1.Start();
-
// task가 끝날 때까지 쓰레드 대기
-
task1.Wait();
-
-
// num을 인자로 받고 int를 return하는 task를 생성
-
Task<int> task2 = new Task<int>((num) =>
-
{
-
Console.WriteLine("task action " + num);
-
return (int)num;
-
}, 3);
-
task2.Start();
-
task2.Wait();
-
// task가 끝날 때까지 대기하다가 끝나면 결과값을 출력
-
Console.WriteLine("task result " + task2.Result);
-
}
이 예제는 task를 사용해서 비슷한 처리를 한 예제입니다.
위에서 설명한 ThreadPool 예제와 하는 일은 비슷하지만 매우 직관적이고 코드도 짧습니다.
사용법은 오히려 가장 앞에서 설명한 Thread와 비슷합니다.
함수를 인자로 해서 Task를 생성하고 Start 함수를 호출한후 Wait 함수로 대기하면 됩니다.
위 Thread 예제와 다른 점이 있다면 내부적으로는 ThreadPool의 Thread를 사용하고 있기 때문에 Thread가 생성/해제 될때 생기는 오버헤드도 거의 없다는 점과 쉽게 리턴 값을 받아올 수 있다는 점입니다.
또 ThreadPool과 다르게 쉽게 Thread 실행 완료를 대기할 수 있습니다.
아무래도 Task가 Thread와 ThreadPool의 단점을 보완하기 위해 새로 만들어진 것이기 때문이겠죠.
따라서 특별한 경우가 아니라면 Task를 사용하시는 것이 좋습니다.
이렇게 오늘 Thread, ThreadPool, Task 등 각각의 사용법을 알아봤습니다.
쓰레드 자체를 사용하는 것은 어렵지 않지만 제대로된 병렬/비동기 프로그래밍을 구현하는 것은 쉽지 않습니다.
왜냐하면 쓰레드를 사용할 때 주의할 점이 여러 가지 있기 때문입니다.
레이스 컨디션, 데드락 이슈는 물론이고 컨텍스트 스위칭 오버헤드 때문에 오히려 성능이 제대로 안 나오는 경우도 있기 때문입니다.
Thread 관련 심화 내용에 대해서는 앞으로 기회가 있을 때 더 포스팅해보도록 하겠습니다.
'프로그래밍' 카테고리의 다른 글
Microsoft PIX를 활용한 DirectX12 프로그램 디버깅 (0) | 2025.01.13 |
---|---|
DirectX12를 활용한 PostProcessing (0) | 2024.12.28 |
로슬린 MakeConst 튜토리얼 (2/2) (0) | 2022.01.26 |
C#의 StackTrace (0) | 2021.08.27 |
로슬린 MakeConst 튜토리얼 (1/2) (0) | 2021.08.27 |
- Total
- Today
- Yesterday
- DirectX12
- normalized device coordinate
- Unity
- opengl
- RL
- perspective projection
- collision detection
- 유니티
- RubiksCube
- 참조 형식
- NDC
- 루빅스큐브
- Scriptable Render Pipeline
- MeshProcessing
- 값 형식
- reference type
- 강화학습
- 최적화
- transform
- Mesh
- Unreal
- Bounding Volume Hierarchy
- Mesh Processing
- value type
- SRP
- AABB
- Transformation
- VTK
- C#
- CollisionDetection
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |