C# :: Lecture & TIPs

[C# .NET] Parallel 로 병렬 처리하기

C#.NET 으로 이것 저것 프로젝트를 진행하면서, 작게는 수십 번, 많게는 수천여 번 반복되는 동일한 프로세스를 가능한 빠른 시간 안에 처리할 수 있는 로직을 작성해야 됐습니다. 동일한 프로세스를 반복하는 경우, 반복 문인 for 문이나 while 문으로 간편하게 처리를 할 수 있지만, 가능한 최단 시간 안에 처리하여야 한다는 전제 조건이 있었기 때문에 ‘어떻게 하면 보다 효율적으로 처리할 수 있을까?’ 하고 꽤나 고민을 했었습니다.

그 결과, 반복문 자체를 병렬 처리하는 것으로 꽤나 긴 시간을 단축할 수 있겠다는 결론으로 도달했었는데요, ‘어떻게 병렬 처리하는 것이 좋을까?’ 하고 알아보니 .NET Framework 4.0 부터는 PFX (Parallel Framework) 라고 불리우는 병렬 처리 프레임워크가 System.Threading.Task 에 추가되어 있었습니다.

 

 

Parallel 클래스를 통해서 정말 간단한 코드로 처리해야 되는 프로세스를 분할하고, CPU 코어 및 스레드에 맞추어 분산하여 병렬 처리, 다시 말해 보다 빠른 시간 안에 프로세스를 끝마칠 수 있도록 기본 클래스를 제공하고 있었는데요, 간단하게 코드를 통해서 살펴보겠습니다.

 

Parallel.For(0, 1000, (index) =>
{
    // 코드 작성
});

Parallel.ForEach(list, index =>
{
    // 코드 작성
});

Parallel.Invoke(
    () => { /* 코드 작성 */ },    
    () => { /* 코드 작성 */ },    
    () => { /* 코드 작성 */ },    
    () => { /* 코드 작성 */ },    
    () => { /* 코드 작성 */ }    
);

 

Parallel 클래스 안에는 For, ForEach, Invoke 총 세 가지 메서드가 포함되어 있습니다.

Parallel.For, Parallel.ForEach 같은 경우 우리가 흔히 반복 처리에 사용하는 For 문과 ForEach 문과 약간의 문법 차이가 있을 뿐 사용 방법 자체는 동일합니다만, 처리 결과 및 과정에 있어서는 분명한 차이가 존재합니다.

 

Parallel.For(시작 값, [종료 값, (인덱스) => { });
Parallel.ForEach(반복하는 리스트 또는 배열, 인덱스 => { });

 

간단하게 예를 들어, 0 부터 3000 까지 있는 프로세스를 처리해야 되는 상황이라고 할 때, For 문이나 ForEach 문 같은 경우 0 번 부터 3000 번 까지 순차적으로 처리를 진행합니다. 하지만, Parallel.ForParallel.ForEach 의 경우 0 번부터 3000 번 까지 순차적으로 진행하는 것이 아니라, CPU가 가지고 있는 코어 및 스레드에 나누어서 병렬 처리가 이루어집니다. 이에 따라서 디버깅 결과는 뒤죽박죽 올라오지만 처리 속도만큼은 정말 확실하게 차이를 보여줍니다.

간단하게 예시 코드를 통해서 확인해보도록 합시다.

 

static void Main(string[] args)
{
    int MAX = 10000000;
    ForLoop(MAX);
    ParallelForLoop(MAX);
}
static void ForLoop(int MAX)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < MAX; i++)
    {
        Console.Write("{0}: {1}", Thread.CurrentThread.ManagedThreadId, i);
    }
    watch.Stop();
    Console.WriteLine("\nFor Loop Time : " + watch.Elapsed.ToString());
}
static void ParallelForLoop(int MAX)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    Parallel.For(0, MAX, (i) =>
    {
        Console.Write("{0}: {1}", Thread.CurrentThread.ManagedThreadId, i);
    });
    watch.Stop();
    Console.WriteLine("\nParallel For Loop Time : " + watch.Elapsed.ToString());
}

 

정말 단순하게 코드를 작성해 0 부터 10,000,000 까지 숫자를 세는 작업에 소요되는 시간도 직접 측정해보았는데요, For 문이 사용된 코드의 경우 약 5 분 18 초가 소요된 반면, Parallel.For 가 적용된 코드는 약 4 분 55 초 소요된 것을 확인할 수 있습니다. 데이터 처리를 순차적으로 진행하는 것과 병렬로 처리하는 것의 속도 차이는 경미하지만 현저한 차이가 있음을 증명하고 있습니다.

다만, 처리해야 되는 데이터 양이 적거나 프로세스가 짧은 경우에 있어서는 순차 처리가 병렬 처리보다 더 빠를 수 있습니다. 이는 병렬 처리를 위해 처리를 분할하는 과정에서 발생하는 시간 차이입니다. Parallel.For 문과 Parallel.ForEach 문은 어떠한 형태 혹은 규모의 데이터나 프로세스를 처리하느냐에 따라 적절하게 활용하는 것이 매우 중요하다고 볼 수 있겠습니다.

최종적으로 Parallel.Invoke 는 여러 코드를 Action Delegate 로 받아들인 후 다중 스레드를 통해 병렬로 작업 (Task) 를 나누어서 1 회 실행하여 병렬 처리하는 기능을 제공하고 있습니다. 즉, 여러가지 코드를 한 번만에 실행하여야 하는 조건에서 십 분 활용할 수 있습니다.

 

Parallel.Invoke(
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); },
    () => { Console.WriteLine("{0}: {1}.{2}", Thread.CurrentThread.ManagedThreadId, DateTime.Now, DateTime.Now.Millisecond); }
);

 

지금까지 Parallel 을 활용한 C# 의 병렬 처리 방법에 대해 한 타래의 게시글로 정리해보았습니다. 상황에 따라 적절하게 활용한다면 매우 효율적인 방법으로 명확하게 입출력을 구분하고, 메모리가 서로 공유되는 과정에서 충돌이 발생하지 않도록 문제 없이 관리가 이루어지도록 코드를 설계한다면, 분명히 보다 빠르고 좋은 성능을 제공하는 코드를 작성할 수 있는 방법입니다.

코드에 따라 약간의 차이가 있겠지만, 제가 진행한 몇 가지 프로젝트에서 적용해본 결과로는 시간을 약 3 ~ 4 배 정도는 거뜬하게 앞당기고 실행 시에도 나은 성능을 활용할 수 있었습니다. 이에, 처리하고자 하는 데이터에 따라 적절하게 사용하길 바라며 이번 게시글을 마무리합니다.

고맙습니다.

 

Leave a Reply

Discover more from Dream big, Achieve more.

Subscribe now to keep reading and get access to the full archive.

Continue reading