들어가며
프로세스에서 스레드를 새로 만들었다 삭제하는 것을 반복하면 스레드 생성에 대한 오버헤드가 발생한다. 그래서 일반적으로 프로세스에 스레드 풀을 생성하고 스레드 작업이 필요한 경우 풀에서 스레드를 꺼내와 사용하고 작업이 끝나면 해제하는 것이 아니라 스레드 풀에 되돌려 주는 방식으로 성능의 향상을 꾀한다.
C#에서는 이런 스레드풀을 언어 레벨에서 지원한다. 스레드 수행 할 작업을 스레드 풀의 큐에 넣으면 내부적으로 스레드를 생성하거나 기존 생성되어 있던 스레드를 가져와 작업을 수행 후 스레드를 삭제하는 것이 아니라 대기 상태로 유지 시킨다. CLR[?]은 이 스레드풀에 최적의 스레드 개수를 유지하도록 관리해준다.
ThreadPool 사용
using System;
using System.Threading;
class Program
{
public static void Foo(object arg)
{
Console.WriteLine($"Foo:{arg}, {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(3000);
}
public static void Main()
{
ThreadPool.QueueUserWorkItem(Foo, "Hello World");
ThreadPool.QueueUserWorkItem(Foo); // arg에 null 전달
Console.ReadLine();
}
}
- ThreadPool에 작업을 요청하기 위해선 void Foo(object arg)와 같이 리턴 타입 void, object를 인자로 받는 메소드 형태여야 한다.
- ThreadPool에 작업을 요청하기 위해선 QueueUserWorkItem 메소드를 이용한다.
아무런 인자도 지정하지 않으면 arg인자로 null이 전달 된다.
ThreadPool의 제약 사항
풀링을 이용해 스레드 자원의 생성과 삭제 오버헤드를 줄여 주고, 최적의 스레드 개수를 유지 할 수 있다는 장점이 있는 반면에 아래와 같은 단점도 존재한다.
- Name 필드 사용 불가능
ThreadPool 내의 스레드는 언제든지 재사용 될 수 있기 때문에 특정한 이름을 지정하는 것이 불가능하다. - ThreadPool에서 생성되는 스레드들은 항상 백그라운드 스레드다
C# 프로세스 종료 시점은 "모든 foreground 스레드가 종료 되는 시점"이다. 하지만 ThreadPool에 생성 되는 스레드들은 모두 background 스레드다. 따라서 ThreadPool에 아직 작업 중인 스레드가 있다고 하더라도 프로세스는 종료 될 수 있다.
또한 Thread의 Join과 같은 메소드도 사용 할 수 없기 때문에 프로세스의 종료 시점까지 대기 할 수도 없다. 그래서 ThreadPool은 작업의 완료 여부를 알 필요 없는 짧은 작업을 처리하는데 주로 사용된다.
마치며
분명히 C# 빌드인 스레드풀 클래스는 우리가 직접 스레드풀을 만들고 관리 할 필요성을 덜어주는 편리한 도구가 맞긴하다. 하지만 앞에 언급된 제약사항이 있는것 또한 사실이다. 그래서 C#에서는 Task라는 해결책을 내놓았다. 시간이 된다면 이 글에 이어지는 [C#] 비동기 프로그래밍 - Task 항목을 추가로 살펴 보도록 하자.