Computer >> 컴퓨터 >  >> 프로그래밍 >> C#

C# Parallel.ForEach 병렬 처리 제한하기 - MaxDegreeOfParallelism 활용법

개요

C#에서 대량의 데이터를 처리할 때 Parallel.ForEach를 사용하면 여러 스레드가 동시에 작업을 수행하여 처리 속도를 크게 향상시킬 수 있습니다. 하지만 기본 설정으로 실행하면 시스템이 사용 가능한 모든 코어를 활용해 스레드를 생성하기 때문에, 경우에 따라 CPU 자원을 과도하게 점유하거나 다른 프로세스에 영향을 줄 수 있습니다. 이 글에서는 일반 foreach 루프와 Parallel.ForEach의 차이점을 살펴보고, MaxDegreeOfParallelism 옵션을 통해 병렬 처리 수준(병렬성)을 제한하는 방법을 알아보겠습니다.

foreach와 Parallel.ForEach의 차이

일반 foreach 루프

foreach 루프는 C# 1.0부터 제공되는 기본 문법으로, 단일 스레드에서 실행되며 항목을 하나씩 순차적으로 처리합니다. 코드 흐름을 예측하기 쉽고 디버깅이 간단하다는 장점이 있지만, 대부분의 경우 Parallel.ForEach에 비해 실행 속도가 느립니다.

Parallel.ForEach 루프

Parallel.ForEach는 C# 4.0부터 System.Threading.Tasks 네임스페이스를 통해 제공되는 기능으로, 여러 스레드에서 동시에 작업을 수행하여 병렬 처리를 구현합니다. 데이터 양이 많거나 각 항목의 처리 비용이 클 때 성능상 큰 이점을 얻을 수 있습니다.

예제 1: foreach와 Parallel.ForEach 성능 비교

아래 예제는 A부터 Z까지 알파벳 리스트를 두 가지 방식으로 출력하면서, 각각 어떤 스레드 ID로 실행되는지와 소요 시간을 측정합니다.

static void Main(string[] args){
    List<string> alphabets = new List<string>();
    for (char c = 'A'; c <= 'Z'; c++)
    {
        alphabets.Add(c.ToString());
    }

    Console.WriteLine("Printing list using foreach loop\n");
    var stopWatch = Stopwatch.StartNew();

    foreach (string alphabet in alphabets)
    {
        Console.WriteLine("alphabet Name: {0}, Thread Id= {1}",
            alphabet, Thread.CurrentThread.ManagedThreadId);
    }
    Console.WriteLine("foreach loop execution time = {0} seconds\n",
        stopWatch.Elapsed.TotalSeconds);

    Console.WriteLine("Printing list using Parallel.ForEach");
    stopWatch = Stopwatch.StartNew();

    Parallel.ForEach(alphabets, alphabet =>
    {
        Console.WriteLine("alphabet Name: {0}, Thread Id= {1}",
            alphabet, Thread.CurrentThread.ManagedThreadId);
    });

    Console.WriteLine("Parallel.ForEach() execution time = {0} seconds",
        stopWatch.Elapsed.TotalSeconds);
    Console.ReadLine();
}

실행 결과 - foreach 루프

foreach 루프는 모든 항목이 동일한 스레드(ID: 1)에서 순차적으로 처리됩니다.

alphabet Name: A, Thread Id= 1
alphabet Name: B, Thread Id= 1
alphabet Name: C, Thread Id= 1
... (중략) ...
alphabet Name: Z, Thread Id= 1
foreach loop execution time = 0.0223421 seconds

실행 결과 - Parallel.ForEach

반면 Parallel.ForEach는 여러 스레드(1, 4, 5, 6, 7번 등)가 동시에 작업을 나누어 처리하며, 출력 순서도 실행마다 달라지는 것을 확인할 수 있습니다.

alphabet Name: A, Thread Id= 1
alphabet Name: G, Thread Id= 4
alphabet Name: H, Thread Id= 4
... (중략) ...
alphabet Name: W, Thread Id= 6
alphabet Name: X, Thread Id= 6
Parallel.ForEach() execution time = 0.0559777 seconds

참고: 위 결과는 항목 수가 적고 출력(Console.WriteLine) 오버헤드가 상대적으로 커서 오히려 Parallel.ForEach가 느리게 측정된 사례입니다. 실제로는 CPU 집약적이거나 반복 횟수가 많은 작업에서 병렬 처리가 월등히 빠릅니다.

예제 2: MaxDegreeOfParallelism으로 병렬성 제한하기

병렬 처리에 사용되는 스레드 수를 제한하려면 ParallelOptions 객체의 MaxDegreeOfParallelism 속성을 설정하면 됩니다. 아래 예제는 최대 2개의 스레드만 사용하도록 제한한 코드입니다.

static class Program{
    static void Main(string[] args){
        List<string> alphabets = new List<string>();
        for (char c = 'A'; c <= 'Z'; c++)
        {
            alphabets.Add(c.ToString());
        }

        Parallel.ForEach(
            alphabets,
            new ParallelOptions { MaxDegreeOfParallelism = 2 },
            alphabet =>
            {
                Console.WriteLine("alphabet Name: {0}, Thread Id= {1}",
                    alphabet, Thread.CurrentThread.ManagedThreadId);
            }
        );
    }
}

실행 결과

출력 결과를 보면 스레드 ID가 1과 4, 즉 최대 2개의 스레드만 사용된 것을 확인할 수 있습니다.

alphabet Name: N, Thread Id= 4
alphabet Name: O, Thread Id= 4
alphabet Name: P, Thread Id= 4
alphabet Name: A, Thread Id= 1
alphabet Name: B, Thread Id= 1
... (중략) ...
alphabet Name: G, Thread Id= 1
alphabet Name: M, Thread Id= 4

정리

  • foreach: 단일 스레드에서 순차 처리, C# 1.0부터 지원, 흐름 예측이 쉬움
  • Parallel.ForEach: 다중 스레드 병렬 처리, C# 4.0 이상에서 System.Threading.Tasks 필요
  • 병렬성 제한: ParallelOptions의 MaxDegreeOfParallelism 값을 지정하면 동시 실행 스레드 수를 조절할 수 있음

MaxDegreeOfParallelism은 데이터베이스 연결 수 제한, 외부 API 호출 rate limit 준수, UI 응답성 유지 등 스레드 과다 생성을 방지해야 하는 상황에서 특히 유용합니다. 값은 정확한 숫자뿐 아니라 ParallelOptions.MaxDegreeOfParallelism = -1(기본값, 무제한)로 설정할 수도 있습니다.