[C#] 37. ThreadPool의 사용법
안녕하세요. 명월입니다.
이 글은 C#에서 ThreadPool의 사용법에 대한 글입니다.
이전 글에서 Thread에 대해서 설명했습니다.
link - [C#] 36. 쓰레드(Thread)를 사용하는 방법, Thread.Sleep 함수 사용법
Thread란 프로그램 내에서 병렬처리를 위한 기능입니다.
그런데 이 Thread는 개수를 제어하지 못하면 오히려 프로그램 성능이 떨어진다는 것도 설명했습니다. 그러기 위해서 Thread 개수를 관리하는 것을 만들어야 하는데, .Net framework에서는 이 Thread의 개수를 관리해 주는 기능이 있는 데 그것이 ThreadPool입니다.
using System;
using System.Threading;
namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
class Node
{
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
// 쓰레드 실행 함수
static void ThreadProc(Object callBack)
{
// 파라미터 값이 Node 클래스가 아니면 종료
if (callBack.GetType() != typeof(Node))
{
return;
}
// Node 타입으로 강제 캐스트(자료형이 Object 타입)
var node = (Node)callBack;
// 설정된 반복문의 횟수만큼
for (int i = 0; i < node.Count; i++)
{
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 완료 콘솔 출력
Console.WriteLine("Complete " + node.Text);
}
// 실행 함수
static void Main(string[] args)
{
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
// 참고, 두번째 파라미터는 비동기 I/O 쓰레드 개수입니다.(그냥 0으로 설정해도 무방)
if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
{
// ThreadPool에 등록, 델리게이트 함수로 ThreadProc를 등록
// 파라미터로 Node 인스턴스를 생성해서 넘긴다.
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "A", Count = 3, Tick = 1000 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "B", Count = 5, Tick = 10 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "C", Count = 2, Tick = 500 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "D", Count = 7, Tick = 300 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "E", Count = 4, Tick = 200 });
}
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}
위 예제를 보시면 제가 ThreadPool 클래스를 이용해서 쓰레드를 총 5개 실행합니다.
그러나 ThreadPool에서의 쓰레드 갯수는 0개에서 최대 2개까지 생성되게 설정했습니다. 즉, Thread를 무제한으로 생성하는 게 아니고 Pool안에서 총량을 설정해서 그 이상은 대기 상태로 넘깁니다.
즉, 결과를 봐도 A와 B는 동시에 실행되지만, C부터는 B가 종료된 시점부터 실행되는 것을 확인할 수 있습니다. Thread를 최대 2개까지 생성하고 그 이상은 실행하지 않는 설정으로 ThreadPool를 관리합니다.
ThreadPool에서는 Thread의 생성을 관리하게 됩니다.
그런데 이 ThreadPool은 Join 함수가 없어서 Thread가 모두 종료할 때까지 기다리는 함수가 없습니다.
using System;
using System.Threading;
namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
class Node
{
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
// 쓰레드 실행 함수
static void ThreadProc(Object callBack)
{
// 파라미터 값이 Node 클래스가 아니면 종료
if (callBack.GetType() != typeof(Node))
{
return;
}
// Node 타입으로 강제 캐스트(자료형이 Object 타입)
var node = (Node)callBack;
// 설정된 반복문의 횟수만큼
for (int i = 0; i < node.Count; i++)
{
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 완료 콘솔 출력
Console.WriteLine("Complete " + node.Text);
}
// 쓰레드 풀의 Join 설정
static void ThreadPoolJoin(int size)
{
// 무한 루프
while (true)
{
// 1초 대기
Thread.Sleep(1000);
// 현재 대기중인 쓰레드를 갯수를 취득하기 위한 변수
int count = 0;
int iocount = 0;
// 현재 사용 가능한 쓰레드 개수 취득
ThreadPool.GetAvailableThreads(out count, out iocount);
// Max Thread와 같으면 무한 루프 종료
if (count == size)
{
// 무한 루프 종료
break;
}
}
}
// 실행 함수
static void Main(string[] args)
{
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
// 참고, 두번째 파라미터는 비동기 I/O 쓰레드 개수입니다.(그냥 0으로 설정해도 무방)
if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
{
// ThreadPool에 등록, 델리게이트 함수로 ThreadProc를 등록
// 파라미터로 Node 인스턴스를 생성해서 넘긴다.
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "A", Count = 3, Tick = 100 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "B", Count = 5, Tick = 10 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "C", Count = 2, Tick = 500 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "D", Count = 7, Tick = 300 });
ThreadPool.QueueUserWorkItem(ThreadProc, new Node { Text = "E", Count = 4, Tick = 200 });
}
// 총 2개의 쓰레드가 대기 상태가 될때까지 대기
ThreadPoolJoin(2);
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}
위 예제는 ThreadPoolJoin이라는 함수를 만들어서 ThreadPool에 사용할 수 있는 쓰레드 개수를 확인해서 쓰레드 사용 최대량과 같으면 무한 루프가 종료하게 하는 것으로 Join 함수를 만들었습니다.
간단한 프로그램은 위처럼 ThreadPool를 제어할 수 있습니다.
문제는 큰 프로그램이라고 한다면 ThreadPool 클래스 특성이 static이기 때문에 어느 곳에서 사용되는 지 확인하기가 어려운 것입니다.
그러니깐 설계하고 있는 곳에서 ThreadPool 사용이 종료되었다 쳐도, 다른 라이브러리나 클래스에서 ThreadPool를 사용한다고 하면, 대기 상태의 개수만으로 대기를 만드는 건, 예상치 못한 버그를 발생할 수 있겠습니다.
그래서 Task에서 사용되는 EventWaitHandle를 이용하면 부분적으로 ThreadPool를 제어할 수 있습니다.
using System;
using System.Threading;
using System.Collections.Generic;
namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
// EventWaitHandle 클래스의 상속
public class Node : EventWaitHandle
{
// 생성자 설정
public Node() : base(false, EventResetMode.ManualReset) { }
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
// 쓰레드 실행 함수
static void ThreadProc(Object callBack)
{
// 파라미터 값이 Node 클래스가 아니면 종료
if (callBack.GetType() != typeof(Node))
{
return;
}
// Node 타입으로 강제 캐스트(자료형이 Object 타입)
var node = (Node)callBack;
// 설정된 반복문의 횟수만큼
for (int i = 0; i < node.Count; i++)
{
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 완료 콘솔 출력
Console.WriteLine("Complete " + node.Text);
// EventWaitHandle의 Join 설정 종료
node.Set();
}
// Node 클래스의 인스턴스를 추가한 후 List에 등록
static EventWaitHandle AddNode(List<EventWaitHandle> list, string text, int count, int tick)
{
// 인스턴스 생성
var node = new Node { Text = text, Count = count, Tick = tick };
// list에 등록
list.Add(node);
// 인스턴스를 리턴
return node;
}
// 실행 함수
static void Main(string[] args)
{
// Join을 위한 EventWaitHandle 리스트
var list = new List<EventWaitHandle>();
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
// 참고, 두번째 파라미터는 비동기 I/O 쓰레드 개수입니다.(그냥 0으로 설정해도 무방)
if (ThreadPool.SetMinThreads(0, 0) && ThreadPool.SetMaxThreads(2, 0))
{
// ThreadPool에 등록, 델리게이트 함수로 ThreadProc를 등록
// 함수 AddNode를 호출해서 인스턴스 생성하고 위 list에 등록한다.
ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "A", 3, 1000));
ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "B", 5, 10));
ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "C", 2, 500));
ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "D", 7, 300));
ThreadPool.QueueUserWorkItem(ThreadProc, AddNode(list, "E", 4, 200));
}
// list에 있는 EventWaitHandle 함수가 전부 Set이 호출되면 Join 해제
WaitHandle.WaitAll(list.ToArray());
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}
list를 로컬 변수로 지정해서 WaitHandle.WaitAll에서 배열로 변환하게 되면 ThreadPool가 종료될 때까지 제어가 가능합니다.
ThreadPool이 전체 사용 중인 쓰레드 개수를 제어할 수 있다는 점은 꽤 편리합니다만, 여기서는 생성한 쓰레드를 기다리는 것을 설정하는게 꽤 불편하네요.
그래서 성능을 위해서는 Thread를 그대로 사용하는 것보다는 ThreadPool를 이용하는게 시스템 성능을 위해서는 더 나은 선택일 수 있습니다.
참고로 위 SetMinThreads 함수와 SetMaxThreads 함수로 쓰레드 개수의 제어를 설정해도 Thread로 생성되는 쓰레드에는 영향이 없습니다. 즉 2개만 설정해도 Thread로는 10개를 설정할 수 도 있다는 뜻입니다.
여기까지 C#에서 ThreadPool의 사용법에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.