Home Full Site
병렬 프로그래밍 (Parallel Programming)

CPU가 하나였던 시대에서 2개, 4개의 CPU를 장착하는 것이 보편화 됨에 따라, 이러한 복수 CPU를 충분히 활용하기 위한 프로그래밍 기법에 대한 요구가 증가하였다. .NET 4.0에서는 이러한 요구에 부합하기 위해 Parallel Framework (PFX)라 불리우는 병렬 프로그래밍 프레임워크를 추가하였다.
병렬처리는 큰 일거리를 분할하는 단계, 분할된 작업들을 병렬로 실행하는 단계, 그리고 결과를 집계하는 단계로 진행된다. 일거리를 분할하는 방식은 크게 Data Parallelism과 Task Parallelism으로 나누어 진다.
Data Parallelism은 대량의 데이타를 처리하는데 있어 각 CPU에 일감을 나눠서 주고 동시에 병렬로 처리하는 것을 말한다. 즉, 대량의 데이타를 분할하여 다중 CPU를 사용하여 다중 쓰레드들이 각각 할당된 데이타를 처리하는데 일반적으로 쓰레드 당 처리 내용은 동일하다. Task Parallelism은 큰 작업 Task를 분할하여 각 쓰레드들이 나눠서 다른 작업 Task들을 실행하는 것이다. Parallel Framework (PFX)은 크게 (1) PLINQ (Parallel LINQ)와 Parallel 클래스 (For/Foreach 메서드)를 중심으로 하는 Data Parallelism 지원 클래스들과 (2) Task, TaskFactory 클래스 등 Task Parallelism을 지원하는 클래스들로 구별할 수 있다. (Parallel.Invoke()는 Task Parallelism을 지원한다)




Parallel 클래스 - Data Parallelism

Parallel 클래스는 Parallel.For()와 Parallel.ForEach() 메서드를 통해 다중 CPU에서 다중 쓰레드가 병렬로 데이타를 분할하여 처리하는 기능을 제공한다. 아래 예제의 첫번째 for 루프는 하나의 쓰레드가 0부터 999까지 순차적으로 처리하게 되는 반면, 두번째 Parallel.For() 문은 시스템이 다중 쓰레드들 생성하여 각 쓰레드가 처리할 데이타를 분할하여 병렬로 실행하게 되기 때문에, 0~999 번호 출력이 뒤죽박죽으로 표시된다. 하지만 0~999까지 각 숫자는 단 한번만 출력된다.

예제

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ParallelApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1. 순차적 실행
            // 동일쓰레드가 0~999 출력
            //
            for (int i = 0; i < 1000; i++)
            {
                Console.WriteLine("{0}: {1}",
                    Thread.CurrentThread.ManagedThreadId, i);                
            }
            Console.Read();

            // 2. 병렬 처리
            // 다중쓰레드가 병렬로 출력
            //
            Parallel.For(0, 1000, (i) => {                
                Console.WriteLine("{0}: {1}",
                    Thread.CurrentThread.ManagedThreadId, i);                
            });
        }
    }
}



순차처리 vs Parallel 병렬처리

대량의 데이타를 여러 쓰레드가 나눠서 처리하는 병렬 처리는 많은 경우 순차적으로 처리하는 것보다 빠를 가능성이 크다. 아래 예제는 대량의 데이타를 암호화(여기서는 초보적인 암호방식인 시저암호를 사용)하는데 있어서 순차적으로 처리한 경우와 Parallel 클래스를 이용해서 병렬처리한 경우를 보여준다. 4 CPU 머신에서 테스트한 결과, 순차처리는 8.7초, 병렬처리는 6.1초로 차이를 보였다.

예제

const int MAX = 10000000;
const int SHIFT = 3;

static void SequentialEncryt()
{            
    // 테스트 데이타 셋업
    // 1000 만개의 스트링
    string text = "I am a boy. My name is Tom.";
    List<string> textList = new List<string>(MAX);
    for (int i = 0; i < MAX; i++)
    {
        textList.Add(text);
    }

    // 순차 처리 (Test run: 8.7 초)
    System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
    watch.Start();
    for (int i = 0; i < MAX; i++)
    {
        char[] chArr = textList[i].ToCharArray();

        // 모든 문자를 시저 암호화
        for (int x = 0; x < chArr.Length; x++)
        {
            // 시저 암호
            if (chArr[x] >= 'a' && chArr[x] <= 'z')
            {
                chArr[x] = (char)('a' + ((chArr[x] - 'a' + SHIFT) % 26));
            }
            else if (chArr[x] >= 'A' && chArr[x] <= 'Z')
            {
                chArr[x] = (char)('A' + ((chArr[x] - 'A' + SHIFT) % 26));
            }
        }

        // 변경된 암호로 치환
        textList[i] = new String(chArr);
    };
    watch.Stop();
    Console.WriteLine(watch.Elapsed.ToString());
}

static void ParallelEncryt()
{
    // 테스트 데이타 셋업
    // 1000 만개의 스트링
    string text = "I am a boy. My name is Tom.";
    List<string> textList = new List<string>(MAX);
    for (int i = 0; i < MAX; i++)
    {
        textList.Add(text);
    }

    // 병렬 처리 (Test run: 6.1 초)
    System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch();
    watch.Start();
    Parallel.For(0, MAX, i =>
    {
        char[] chArr = textList[i].ToCharArray();

        // 모든 문자를 시저 암호화
        for (int x = 0; x < chArr.Length; x++)
        {
            // 시저 암호
            if (chArr[x] >= 'a' && chArr[x] <= 'z')
            {
                chArr[x] = (char)('a' + ((chArr[x] - 'a' + SHIFT) % 26));
            }
            else if (chArr[x] >= 'A' && chArr[x] <= 'Z')
            {
                chArr[x] = (char)('A' + ((chArr[x] - 'A' + SHIFT) % 26));
            }
        }

        // 변경된 암호로 치환
        textList[i] = new String(chArr);
    });
    watch.Stop();
    Console.WriteLine(watch.Elapsed.ToString());
}



Parallel.Invoke()

Parallel.Invoke() 메서드는 여러 작업들을 병렬로 처리하는 기능을 제공한다. 즉, 다수의 작업 내용을 Action delegate로 받아 들여 다중 쓰레드들로 동시에 병렬로 Task를 나눠서 실행하게 된다. Task 클래스와 다른 점은, 만약 1000개의 Action 델리게이트가 있을 때, Task 클래스는 보통 1000개의 쓰레드를 생성하여 실행하지만(물론 사용자 다르게 지정할 수 있지만), Parallel.Invoke는 1000개를 일정한 집합으로 나눠 내부적으로 상대적으로 적은(적절한) 수의 쓰레드들에게 자동으로 할당해서 처리한다.

예제

static void RunTasks()
{
    // 5개의 다른 Task들을 병렬로 실행
    Parallel.Invoke(
        () => { method1(); },
        () => { method2(); },
        () => { method3(); },
        () => { method4(); },
        () => { method5(); }
    );

    //...
}



© csharpstudy.com