방프리
24.04.07 Chapter4. 병렬 처리 (Item 37) 본문
Item 37 : 스레드를 생성하지 말고 스레드 풀을 사용하라
스레드 개수를 개발자가 관리하기엔 무리가 있다. CLR에서 가비지 컬렉터 사용을 위해 스레드를 얼마나 할당해야되는지 알 수 없고 이외에도 Task를 관리하기 위해 얼마나 생성해야하는지 모르기 때문이다.
그러기에 우리는 스레드풀 세팅을 통해 스레드들이 스레드 풀에서 관리하도록 해야한다. 또한 스레드 풀이 태스크의 생명주기도 관리하기 때문에 스레드 풀을 적극적으로 사용해야 한다.
스레드 풀이 태스크의 작업 진행 상황을 리소스에 기초하여 할당하고 부하를 적당히 분산하기에 부하 분산 로직을 개발자가 만들 필요가 없다. 적절한 스레드의 개수를 찾아내는 건 코어 수에 비례해 찾는건 너무 고전적인 해법이라 필자는 이야기 한다. 그럼 어떤 방식으로 찾아야할까?
책에서는 제곱근 게산 알고리즘인 헤론의 공식을 예로 들었다. 특정 숫자의 제곱근의 근사값을 설장하는 것부터 시작하여 다음 단계의 근사값을 현재 근삿값과 원래 값을 근사값으로 나눈 것의 평균을 구하는 것이다. 예로 1로 시작하였다면 다음 근사값은 (1 + (10 / 1)) / 2 = 5.5가 된다.
// 헤론의 공식
public static class Hero
{
public static double FindRoot(double number)
{
double previousError = double.MaxValue;
double guess = 1;
double error = Math.Abs(guess * guess - number);
while (previousError / error > 1.000001)
{
guess = (number / guess + guess) / 2.0;
previousError = error;
error = Math.Abs(guess * guess - number);
}
return guess;
}
}
// 헤론의 공식을 멀티 스레드로 돌리는 로직
private static double OneThread()
{
Stopwatch start = new Stopwatch();
double answer;
start.Start();
for (int i = LowerBound; i < UpperBound; ++i)
{
answer = Hero.FindRoot(i);
}
start.Stop();
return start.ElapsedMilliseconds;
}
private static async Task<double> TaskLibrary(int numTasks)
{
var itemsPerTask = (UpperBound - LowerBound) / numTasks + 1;
double answer;
List<Task> tasks = new List<Task>(numTasks);
Stopwatch start = new Stopwatch();
start.Start();
for (int i = LowerBound; i < UpperBound; i += itemsPerTask)
{
tasks.Add(Task.Run(() =>
{
for (int j = i; j < i + itemsPerTask; ++j)
{
answer = Hero.FindRoot(j);
}
}));
}
await Task.WhenAll(tasks);
start.Stop();
return start.ElapsedMilliseconds;
}
private static double ThreadPoolThreads(int numThreads)
{
Stopwatch start = new Stopwatch();
using (AutoResetEvent e = new AutoresetEvent(false))
{
int workerThreads = numThreads;
double answer;
start.Start();
for (int thread = 0; thread < numThreads; thread++)
{
System.Threading.ThreadPool.QueueUserWorkItem(
(x) =>
{
for (int i = LowerBound; i < UpperBound; ++i)
if (i % numThreads == thread)
answer = Hero.FindRoot(i);
if (InterLocked.Decrement(ref workerThreads) == 0
e.Set();
});
}
e.WaitOne();
start.Stop();
return start.ElapsedMilliseconds;
}
}
private static double ManualThreads(int numThreads)
{
Stopwatch start = new Stopwatch();
using (AutoResetEvent e = new AutoResetEvent(false))
{
int workerThreads = numThreads;
double answer;
start.Start();
for (int thread = 0; thread < numThreads; ++thread)
{
System.Threading.Thread t = new Thread(
() =>
{
for (int i = LowerBound; i < UpperBound; ++i)
if (i % numThreads == thread)
answer = Hero.FindRoot(i);
if (Interlocked.Decrement(ref workerThreads) == 0
e.Set();
});
t.Start();
}
e.WaitOne();
start.Stop();
return start.ElapsedMilliseconds;
}
}
수동으로 스레드를 생성하는 작업은 스레드 풀이나 Task 기반의 구현체보다 오버헤드가 훨씬 크다. 스레드가 10개 이상일 땐 멀티스레딩 자체가 주요 병목이 된다. 하지만 Task기반 라이브러리는 오버헤드가 일정하다. 스레드 수가 적을 때는 느리지만 요청한 태스크 수가 늘어날수록 다른 알고리즘보다 스레드 수를 더 잘 관리한다.
스레드 풀을 사용해야 하는 큰 이유는 다음과 같다.
1. 스레드 풀은 작업을 수행할 준비가 된 스레드를 재사용한다.
2. 스레드 풀이 활성 스레드의 개수를 자동으로 관리한다. 너무 많이 생성하면 충분한 리소스가 확보될 때까지 일부 태스크를 큐에 대기시킨다.
'C# > More Effective C#' 카테고리의 다른 글
24.04.14 Chapter4. 병렬 처리 (Item 39) (1) | 2024.04.14 |
---|---|
24.04.08 Chapter4. 병렬 처리 (Item 38) (0) | 2024.04.08 |
24.04.03 Chapter4. 병렬 처리 (Item 36) (0) | 2024.04.03 |
24.03.23 Chapter4. 병렬 처리 (Item 35) (1) | 2024.03.23 |
24.03.12 Chapter3. 태스크 기반 비동기 프로그래밍 (Item 34) (0) | 2024.03.12 |