Welcome to Parallel C#(1) - 굿바이, 그리고 안녕~~?

C# Parallel Programming 2010. 5. 24. 09:00 Posted by 알 수 없는 사용자

- 끝은 또 다른 시작일 뿐.

인생에서는 항상 뭔가가 끝나면 뭔가가 시작되기 마련입니다. 직장을 그만두면, 직장인은 끝나지만, 백수가 시작되죠. 직장을 구하면, 백수가 끝나고 직장인을 시작하는 것이구요. 길고 길었으며, 별로 인기 없었던 Welcome to dynamic C#이 끝나고, 앞으로도 얼마나 길고 길지 모르며, 인기도 없을 Welcome to Parallel C#이 시작됩니다. 인생에선 끝이 안나는 것도 있더군요. 허접함은 불치병이라고 들었습니다. 시한부면 좋으련만. 흥. 그래도 이 팀 블로그에서 유일하게 만나실 수 있는 허접함의 향연이니, 나름 즐겁지 않으신가요. 어헣어헣.


- 자 언제나 그렇듯이 개념정리 부터 갑시다.

언제나 그렇죠. 제 글은 언제나 그래영. 우선 개념정리부터 하고 들어갑니당. 이 시리즈가 병렬적 C#에 대한 글이니, 우선 관련된 개념부터 정리하고 들어가야죠. 어헣. 이 바닥에서 흔히 사용되지만 쫌 헷갈리는 용어가 두 개 있죠. 바로, 동시성(Concurrency)와 병렬성(Parallelism)이죠. 여러분은 이거 잘 구분되시나영? 되시면 건너 뛰시구요, 안되면? 이 글은 바로 당신과 나를 위한 글인거죠. 아.. 이건 운명적 만남. 어헣.

이 개념을 잘 정리한 글을 찾아서 인터넷을 뒤졌는데, 그 결과 나름 정리한 결과는 다음과 같습니다.

1. 첫번째 정리(동시성 vs 병렬성)

동시성과 병렬성은 같은 의미가 아니다. 만약, T1과 T2라는 두 개의 작업이 시간상 언제 어떻게 수행될지 미리 정해져있지 않다면, 그 두 작업은 동시적이라고 말할 수 있다. 즉, 

  T1은 T2보다 빨리 수행되고 종료될 수 있다.
  T2는 T1보다 빨리 수행되고 종료될 수 있다.
  T1과 T2는 같은 시간에 동시에 실행될 수 있다.(이거슨 레알 병렬성.)
  T1과 T2는 교차적으로 수행될 수 있다.

2. 두번째 정리(동시적 vs 병렬적)

병렬성이라는 말은 여러개의 동일한 작업을 동시에 수행하는 것을 의미한다.(각각의 동시에 수행되는 동일한 작업들은 서로 독립적이다.) 동시성이라는 것은 여러개의 작업을 공통된 목표를 향해서 동시에 수행하는 것을 의미한다.

둘을 확실히 구분하기 힘든 것은, 동시성을 위해서 병렬성을 활용할 수 있다는 것이다. 예를 들어서 퀵소트 알고리즘을 생각해보면, 퀵소트 알고리즘의 각 분리된 단계는 병렬적으로 정렬될 수 있다. 하지만 전체 알고리즘은 동시적이다. 즉, 퀵소트 알고리즘은 동시적 이면서도(각 분할된 단계에서 나온 결과를 종합해서 하나의 공통된 목표를 추구하므로), 각 분할된 단계의 정렬은 병렬적일 수 있는 것이다. 그리고 병렬적인 각 단계의 정렬은 서로 무관하며, 서로 다른 데이터에 대해서 정렬을 하는 것이다. 그래서 좀 헷갈리긴 하겠지만, 전체 알고리즘을 병렬 퀵소트라고 부를 수도 있는 것이다.



- 넌 여전히 말이 많구나.

즉, 동시성과 병렬성은 서로 다른 개념이지만, 서로 같이 사용되는 경우가 있어서 확실히 구분하기가 애매한 경우도 있다는 말인데요. 어디 한번, 예제를 구경해보면서 이야기를 하시죠.

using System;
using System.Collections.Generic;
using System.IO;

namespace Exam1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> files = new List<string>();
            files.AddRange(Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 10.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 9.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Windows Mobile 6 SDK", "*", SearchOption.AllDirectories));
            List<string> fileList = new List<string>();

            foreach (var file in files)
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 10000000)
                    {
                        fileList.Add(file);
                    }
                }
            }

            foreach (var file in fileList)
            {
                Console.WriteLine(file);
            }
        }
    }
}

<코드1> 일단 그냥 평범한 예제

<코드1>은 그냥 평범한 예제입니다. 명시해준 폴더에 있는 모든 파일을 검색해서 10메가가 넘는 파일의 목록을 만드는 프로그램이죠. 위 코드에서 왜 디렉토리를 저렇게 명시해줬냐고 물으신다면?!? 그냥 권한 때문에 접근 못하는 디렉토리가 있어서, 파일이 많은 디렉토리 위주로 골랐다고 대답해드리지요. 어헣. <코드1>을 동시성을 활용할 수는 없을까요? 만약에 동시성을 활용한다면, 어디를 동시적으로 실행 할 수 있을까요? 정답을 맞추시는 분께는 2박 3일로 하와이!!! 의 사진을 구경할 수 있는 기회를 드리겠습니다. 구글협찬이구요, PC는 개인지참입니다..... 아.. 상품대박.

foreach (var file in files)
{
    FileInfo fileInfo = new FileInfo(file);
    if (fileInfo.Exists)
    {
        if (fileInfo.Length >= 10000000)
        {
            fileList.Add(file);
        }
    }
}
<코드2> <코드1>에서 동시성을 활용가능한 곳!

넵, 바로 <코드2>가 동시성을 활용가능한 곳이지요. 왜냐면, 각 파일이름을 가지고 파일을 가져와서, 파일의 크기를 검사하는 각각의 작업은 서로 전혀 상관없기 때문이죠. '마재윤이조작이라니.jpg'라는 파일과 '박세식바보똥깨.avi'라는 파일에 대해서 동시에 작업이 진행된다고 해서 서로 겹치는 것도 없고, 문제가 될 것도 없습니다. 오히려, 순차적으로 수행할 때보다, 3-4개정도로 쪼개서 동시에 작업을 수행하면 훨씬 빨라지겠죠. 그래서! 여기서 병렬성을 활용하게 됩니다.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Exam1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> files = new List<string>();
            files.AddRange(Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 10.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 9.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Windows Mobile 6 SDK", "*", SearchOption.AllDirectories));
            List<string> fileList = new List<string>();

            Parallel.ForEach(files, (file) =>
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 1000000)
                    {
                        fileList.Add(file);
                    }
                }
            });

            foreach (var file in fileList)
            {
                Console.WriteLine(file);
            }
        }
    }
}

<코드3> 병렬성! 을 활용한 버전.

<코드3>에서 굵게 처리된 부분이 바로 변경된 부분입니다. 나머지는? 똑같죠잉~~. 어헣. 굵게 처리된 부분은 .NET 4.0에서 새로 추가된 부분이며, 추후에 더 자세하게 설명드릴 기회가 있을 것 같군뇨오! 그러니깐 일단 궁금증일랑 고이접어 나빌레라~ 하시고, <코드3>을 봅시당. <코드3>에서 분명 각 리스트를 몇 부분으로 쪼개서 수행시간을 줄이려고 병렬성을 도입했지만, 각 작업의 결과는 한 개의 리스트에 추가됩니다. 즉, 10메가 넘는 파일의 목록을 만든다는 공통의 목표를 달성하기 위해서 병렬성을 사용한 것이죠. 즉, 위 코드는 병렬성을 활용한 동시적 코드입니다. 이게 위에서 열심히 동시성과 병렬성을 설명드린 내용인 거죠.


- 그래서? 얼마나 빠른기고?

자~ 그럼 얼마나 빠른건지 어디 한번 확인해봅시다.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Exam1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> files = new List<string>();
            files.AddRange(Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 10.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Microsoft Visual Studio 9.0", "*", SearchOption.AllDirectories));
            files.AddRange(Directory.GetFiles("C:\\Program Files (x86)\\Windows Mobile 6 SDK", "*", SearchOption.AllDirectories));
            List<string> seqFileList = new List<string>();
            List<string> parFileList = new List<string>();

            DateTime seqStart = DateTime.Now;
            //순차적 방식
            foreach (var file in files)
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 10000000)
                    {
                        seqFileList.Add(file);
                    }
                }
            }
            DateTime seqEnd = DateTime.Now;
            TimeSpan seqResult = seqEnd - seqStart;

            DateTime parStart = DateTime.Now;
            //병렬적 방식
            Parallel.ForEach(files, (file) =>
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 1000000)
                    {
                        parFileList.Add(file);
                    }
                }
            });
            DateTime parEnd = DateTime.Now;
            TimeSpan parResult = parEnd - parStart;

            Console.WriteLine("순차적 방식 : {0}",seqResult);
            Console.WriteLine("병렬적 방식 : {0}", parResult);
        }
    }
}

<코드4> 비교를 해보자!

<코드4>처럼 순차적 방식과 병렬적 방식의 시작 시간과 끝 시간을 기록해서 두 방식에서 걸리는 시간을 측정해봤습니다. 결과능~?

순차적 방식 : 00:00:01.6270960
병렬적 방식 : 00:00:00.6230368
계속하려면 아무 키나 누르십시오 . . .
<결과1> 비교 결과.

그렇쿤뇨. 병렬적 방식이 두배 이상 빠르게 나온 걸 볼 수 있습니다. 그저 기존의 루프를 병렬 루프로 바꾼 것 뿐인데 말이죠.


- 마무리 합시다.

일단 오늘은 기본적인 개념을 명확하게 하고, 아주 간략하게 예제를 봤습니다. 일단 .NET 4.0에서 병렬 프로그래밍이 상당히 편해진 거 같긴하죠? 조금씩 더 자세히 알아보도록 하지요. 어헣.


- 참고자료

1. http://blogs.sun.com/yuanlin/entry/concurrency_vs_parallelism_concurrent_programming
2. http://my.opera.com/Vorlath/blog/2009/10/08/parallel-vs-concurrent
3. Essential C# 4.0, Mark Michaelis, Addison Wisley