- 취소는 어렵지.

약속은 하는 것 보다, 취소하는 게 어렵습니다. 게다가 취소할 때는 적절한 타이밍을 놓치면, 안 좋은 기억만 남기게 되죠. 그래서 프로그램에서도 취소를 제대로 할 수 있도록 지원하는 게 중요합니다. 누구나 실수 할 수 있거든요.


- TPL과 함께 취소 좀 더 쉽게하기. 어헣.

TPL은 두가지 형태의 병렬성을 지원합니다. 첫 번째는 작업 병렬성(Task Parallel)이고, 두 번째는 데이터 병렬성(Data Parallelism)입니다. 작업 병렬성은 하나 이상의 작업이 동시에 진행되는 것을 말하구요, 데이터 병렬성은 연속적인 데이터에 대해서 동일한 작업이 동시적으로 수행되는 것을 말합니다. 기존까지 Task클래스와 관련해서 살펴봤던게 작업 병렬성을 위한 것이었다면, 이번에는 데이터 병렬성을 지원하는 부분을 살펴보겠습니다.

데이터 병렬성을 매우 손쉽게 지원하기 위해서 System.Threading.Tasks.Parallel클래스에 병렬성을 지원하는 for와 foreach를 추가했습니다. Parallel.For와 Parallel.ForEach가 바로 그 것인데요. 하나씩 살펴보겠습니다.

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

namespace Exam13
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 1000).ToArray<int>();
            Parallel.For(0, 1000, (i) =>
            {
                nums[i] = nums[i] * nums[i];
            });

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(nums[i].ToString());               
            }
        }
    }
}

<코드1> 간단한 병렬 예제.

<코드1>을 보면, 1부터 1000까지의 정수 배열을 만든 뒤에, 각각의 수를 제곱하는 코드입니다. i번째의 숫자를 제곱해서 그 결과를 i번째 인덱스에 넣는 작업과, i+1번째의 숫자를 제곱해서 그 결과를 i+1번째 인덱스에 넣는 작업은 별개의 작업이며, 동시에 수행가능한 작업이죠. 저렇게 for와 거의 비슷한 모양으로 작성하고, for대신에 Parallel.For를 써주는 것 만으로도 남아도는 CPU의 코어를 활용할 수 있다니. 간편하죠?

Parallel.ForEach와 병렬 루프에서 예외를 처리하는 부분은 이미 다룬 부분이기 때문에 건너뛰구영. 바로, 병렬 루프를 취소하는 방법에 대해서 알아보겠습니다. 지난 포스트에서 작업을 취소하는 방법에 대해서 알아봤었는데요. 이번에도 크게 다르지 않습니다. 동일하게 CancellationTokenSource와 CancellationToken클래스를 활용합니다. 다만, 방법이 약간 다른데요, 예제를 보시죠.

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

namespace Exam14
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts =
                new CancellationTokenSource();
            ParallelOptions parallelOptions =
                new ParallelOptions
                {
                    CancellationToken = cts.Token
                };
            cts.Token.Register(
                () => Console.WriteLine("Cancelling....")
            );

            Console.WriteLine("끝내려면 엔터키를 누르세용.");

            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());

            Task task = Task.Factory.StartNew(() =>
                {
                    try
                 {
                        Parallel.ForEach(files, parallelOptions,
                        (file) =>
                        {
                            FileInfo fileInfo = new FileInfo(file);
                            if (fileInfo.Exists)
                            {
                                if (fileInfo.Length >= 10000000)
                                {
                                    fileList.Add(fileInfo.Name);
                                }
                            }
                        });
                 }
                 catch (OperationCanceledException)
                 {
                 }
                });

            Console.Read();

            cts.Cancel();
            task.Wait();
           
            foreach (var file in fileList)
            {
                Console.WriteLine(file);
            }
            Console.WriteLine("총 파일 개수 : {0}",fileList.Count());
        }
    }
}

<코드2> 병렬 루프에서의 작업 취소.

<코드2>를 보면, Parallel.ForEach이용해서, 음악 파일 중에서 10메가가 넘는 파일만 찾아서 리스트에 담고 있습니다. 그리고 루프 취소와 모니터링을 위해서 CancellationTokenSource, CancellationToken클래스를 활용하고 있습니다. 다른점이 있다면, 병렬 루프에 옵션을 주기 위해서 ParallelOptions클래스를 사용하고 있다는 것이죠. 그리고 생성된 ParallelOptions타입의 객체에 Token을 주고, 그 객체를 Parallel.ForEach루프에 매개변수로 넘겨주고 있습니다. 결과를 보면, 늦게 취소를 한 경우에는 리스트가 모두 완성된 반면에, 빨리 취소를 한 경우에는 리스트가 만들어지다가 만 걸 확인할 수 있죠.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

05 サンクチュアリ.mp3
06 空のように 海のように.mp3
07 月の虹.mp3
총 파일 개수 : 380
계속하려면 아무 키나 누르십시오 . . .

<결과1> 늦게 취소해서 다 완성된 리스트.

끝내려면 엔터키를 누르세용.
파일 개수 : 2746

Cancelling....

(중략...)

01.mp3
02.mp3
03.mp3
01.うるわしきひと.mp3
총 파일 개수 : 256
계속하려면 아무 키나 누르십시오 . . .

<결과2> 중간에 취소해서 만들어지다 만 리스트.

ParallelOptions를 통해서 CancellationToken을 받은 병렬 루프는 내부적으로, IsCancellationRequested속성을 계속해서 주시하고 있습니다. 그리고, 이 취소 요청이 들어와서 이 속성이 true가 되면, 그 이후로는 새로운 루프가 시작되는 걸 막아버리는 것이죠. 그리고 병렬 루프가 취소되었음을 외부에 알릴 수 있는 유일한 방법이 OperationCanceledException을 통해서 인데요. <코드2>를 보면, catch를 통해서 예외를 잡긴하지만, 무시해버렸습니다. 그래서 Register메서드를 통해서 등록된 "Cancelling...."이라는 메세지가 출력되고 프로그램이 종료된 것이죠.


- 마치면서

역시 병렬처리를 간단하게 만들어 주는 만큼, 병렬처리를 취소하는 방법도 최대한 간단하게 만들어 주네요. TPL만쉐이! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd537608.aspx
3. http://msdn.microsoft.com/en-us/library/dd537609.aspx

저작자 표시 비영리 변경 금지
신고

- 뭐, 미안하다고?

선진국에 가보면, 약간만 부딛힐 듯한 상황이라면, 서로 'Excuse me', '스미마셍'같이 서로를 배려하는 모습을 볼 수 있습니다. 우리나라에서는 아직 길을 걸으면서 뒷 사람에게 담배연기를 선사한다던가, 뭐 그리 급한지 보행자일 때는 운전자를, 운전할 때는 보행자를 씹으면서 급하게 서두르는 모습을 쉽게 볼 수 있습니다. 층간소음이 일어나면 오히려 윗집이 더 큰소리를 치기도 하죠. 시민의식으로 겨루는 한일전에서도 완승을 거뒀으면 좋겠다는 생각을 합니다만, 저 역시 모범시민은 아니기에 같이 노력해야겠죠. 어허허허헣. 오늘은 닷넷이 예절을 얼마나 잘 지키는 지, 한번 살펴보겠습니다.


- Stop it, Now!

위 소제목을 보시고, 잭 바우어를 떠올렸다면, 24시의 팬이시겠군요. 잭 바우어는 너무나도 급한 상황을 많이 만나는데요, 상대방에게는 정말 미안하지만, 상황을 해결하기 위해서 윽박지르고, 때로는 때리고, 아주 때로는 다리를 쏘는 등등등! 의 방법을 사용합니다.

아흙. 닷넷의 멀티 스레드 환경을 한번 생각해보죠. 여러개의 스레드가 작업을 처리하는 동안, 하나의 스레드는 사용자의 UI에서 입력을 기다립니다. 그리고 사용자가 취소버튼을 누르면, 사용자의 의지를 이어받아서 다른 스레드들을 취소해야 하는데요. 기존의 .NET 3.5까지는 작업 중인 스레드를 취소하는 게 매우 무례했습니다. 취소해야 할 때는 기냥 바로 끼어들어서 취소해버렸기 때문이죠. 그렇게 하면, 데이터 업데이트가 이뤄지는 도중에 취소되어서 부분적으로만 데이터가 업데이트 된다든지, 자원해제가 제대로 안 된다든지 하는 부작용의 위험이 항상 존재합니다. 그래서 가능하면, 다른 방법이 전혀 없을 때, 이렇게 하는 것이 좋겠죠?

물론 기존의 방식도 여전히 활용가능하지만, 이젠 닷넷이 많이 예의를 갖췄습니다. 닷넷 4.0에 새롭게 추가된 PLINQ나 TPL을 사용하는 경우에는 취소 요청 접근법(cancellation request approach)만 사용할 수 있는데요, 이런 방식을 협력적인 취소(cooperative cancellation)이라고 합니다. 즉, 한 스레드가 다른 스레드를 강제로 종료시키는 게 아니라, 작업 취소 API를 통해서 작업을 취소해줄 것을 요청하는 것이죠. 취소 플래그를 통해서 취소요청을 받은 작업은 취소요청에 어떻게 응답할 것인지 선택할 수 있습니다. 예제를 하나 보시죠.

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

namespace Exam11
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            cancellationToken.Register(Canceled);

            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }
        }

        static void Canceled()
        {
            Console.WriteLine("작업이 취소되었군요!!");
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token));
           
            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            task.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드1> 취소 요청 접근법.

<코드1>은 그냥 평범하게 '-'를 출력하는 예제입니다. 하지만, 새로운 클래스가 몇개 보이는데요. CancellationTokenSource, CancellationToken클래스말이죠.

namespace System.Threading
{
    [ComVisible(false)]
    [DebuggerDisplay("IsCancellationRequested = {IsCancellationRequested}")]
    public struct CancellationToken
    {
        public CancellationToken(bool canceled);
       
        public static bool operator !=(CancellationToken left, CancellationToken right);

        public static bool operator ==(CancellationToken left, CancellationToken right);

        public bool CanBeCanceled { get; }

        public bool IsCancellationRequested { get; }

        public static CancellationToken None { get; }

        public WaitHandle WaitHandle { get; }

        public bool Equals(CancellationToken other);

        public override bool Equals(object other);

        public override int GetHashCode();

        public CancellationTokenRegistration Register(Action callback);

        public CancellationTokenRegistration Register(Action<object> callback, object state);

        public CancellationTokenRegistration Register(Action callback, bool useSynchronizationContext);

        public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext);

        public void ThrowIfCancellationRequested();
    }
}

<코드2> 구조체 CancellationToken.

CancellationToken클래스는 말 그대로, 현재 이 토큰이 어떤 상태에 있는지 모니터링 하기 위한 정보를 갖고 있습니다. 이 토큰이 현재 취소 요청을 받았는지, 취소요청을 받으면 어떤 행동을 취할 것인지 등을 확인하고, 설정할 수 있습니다.

namespace System.Threading
{
    [ComVisible(false)]
    public sealed class CancellationTokenSource : IDisposable
    {
        public CancellationTokenSource();

        public bool IsCancellationRequested { get; }

        public CancellationToken Token { get; }

        public void Cancel();

        public void Cancel(bool throwOnFirstException);

        public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens);

        public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2);

        public void Dispose();
    }
}

<코드3> CancellationTokenSource 클래스

그리고 CancellationTokenSource는 CancellationToken의 기반이 되는 클래스로(Source라는 이름이 붙어있죠), CancellationTokenSource에서 생성된 각각의 Token에 대해서 취소를 요청하는 역할을 합니다. CancellationTokenSource에서 Cancel메서드로 취소요청을 하면, 같은CancellationTokenSource에서 생성된 Token들은 전부 취소요청을 받는 셈이죠.


한가지 주목해서 보실 점은 CancellationToken가 클래스가 아니라 구조체라는 것입니다. 즉, Token을 매번 다른 객체에 넘겨줄 때마다 새로운 복사본이 생성된다는 것이죠. 그래서 각각의 스레드에 넘겨진 Token은 각각 독립적인 복사본 이므로, Cancel메서드는 스레드 안전성(thread-safe)을 확보할 수 있습니다. 만약에 참조가 그냥 복사된다면, 각각의 스레드가 Token에 손을 대면, 다른 스레드가 참조하는 Token에도 동일한 변화가 생겨서 예측불가능한 일이 벌어지겠죠.

<코드1>을 보면, 병렬적으로 수행되는 작업에서 취소 요청을 모니터링하기 위해서, CancellationToken을 인자로 넘겨주는 것을 볼 수 있습니다. 그래서 PrintDash메서드 내부에서 IsCancellationRequested속성을 통해서 작업 취소 요청이 들어왔는지 계속 해서 확인하게 되죠. 그럼 <코드1>을 실행 해볼까요?

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--작업이 취소되었군요!!
*******************************************************************************
작업의 완료상태 : RanToCompletion

계속하려면 아무 키나 누르십시오 . . .

<결과1> <코드1>의 실행결과.

<결과1>을 보면, 작업의 완료상태를 출력하는 부분이 있는데요, 이 부분에서 RanToCompletion이 출력되고 있습니다. 그래서 만약, ContinueWith메서드로 연쇄 작업을 연결하고, 옵션을 OnlyOnCanceled로 설정해준다고 하더라도, 연쇄작업은 실행되지 않습니다. 작업은 완료된 상태이기 때문에, 연쇄 작업이 취소되었다는 에러메세지만 확인할 수 있을 뿐이죠. 그렇다면, 연쇄작업을 이용해서 <코드1>과 동일한 결과를 내려면 어떻게 해야 할까요?

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

namespace Exam12
{
    class Program
    {
        static void PrintDash(CancellationToken cancellationToken)
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                Console.Write("-");
            }

            if (cancellationToken.IsCancellationRequested)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }
        }

        static void Main(string[] args)
        {
            string stars = "*".PadRight(Console.WindowWidth - 1, '*');

            CancellationTokenSource cancellationTokenSource =
                new CancellationTokenSource();

            Task task = Task.Factory.StartNew(
                () => PrintDash(cancellationTokenSource.Token),
                cancellationTokenSource.Token);

            Task canceledTask = task.ContinueWith(
                (antecedentTask) => Console.WriteLine("작업이 취소되었군요!!"),
                TaskContinuationOptions.OnlyOnCanceled);

            Console.ReadLine();

            cancellationTokenSource.Cancel();
            Console.WriteLine(stars);
            canceledTask.Wait();
            Console.WriteLine("작업의 완료상태 : {0}", task.Status);
            Console.WriteLine();
        }
    }
}

<코드4> <코드1>과 동일하지만, 연쇄작업을 사용하는 코드.

<코드4>가 바로, <코드1>과 동일한 결과를 내는 코드입니다.(사실 완전히 같지는 않습니다. 실행해보면, '작업이 취소되었군요!'라는 멘트가 출력되는 위치가 다르지요.) 이런식으로 연쇄작업을 연결해놓고, 병렬로 실행되는 메서드 안에서, ThrowIfCancellationRequested()메서드를 통해서, 취소되었을 때, 취소되었다는 표시를 하도록 한 것이죠. 그러면, 연쇄 작업이 바톤을 이어받아서 실행을 계속하게 됩니다. 그리고 또 한가지 차이점은 작업을 생성할 때, 인자로 Token을 넘겨준다는 것이지요.


- 마치면서.

요즘 월드컵을 보면 16강이 가능할 것 같다는 생각이 들기도 하는데요. 꼭! 갔으면 좋겠네요!! ......이게 마무리 멘트라니-_-!! 어헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley

저작자 표시 비영리 변경 금지
신고

- 끝은 또 다른 시작일 뿐.

인생에서는 항상 뭔가가 끝나면 뭔가가 시작되기 마련입니다. 직장을 그만두면, 직장인은 끝나지만, 백수가 시작되죠. 직장을 구하면, 백수가 끝나고 직장인을 시작하는 것이구요. 길고 길었으며, 별로 인기 없었던 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

저작자 표시 비영리 변경 금지
신고
Visual Studio 2008과 Expression Blend 3를 사용 할 때 Visual Studio의 프로젝트/솔루션의 xaml 파일에서 마우스 오른쪽 버튼 클릭 후 Open in Expression Blend 메뉴를 클릭하면 Blend 3가 열리면서 디자인 작업 하는 방법을 많이 사용하실겁니다.

이번에 PDC09에서 Silverlight 4가 발표되면서 VS2010과 Blend Preview for .NET 4를 설치 하신 분들이 많을텐데 VS2010에서 Open in Expression Blend 메뉴를 클릭하면 Blend Preview for .NET으로 열리는게 아니고 Blend 3로 열리는것을 경험 할 수 있습니다.

기존에 설치된 Blend 3 때문에 그런 줄 알았는데 VS2010과 Blend Preview for .NET 4만 클린 설치 해도 Blend가 실행 되지 않는다고 합니다. 이 경우에는 Open in Expression Blend 메뉴가 나타나지 않는다고 하네요.


결론적으로 Blend 3가 함께 설치된 경우에만 메뉴가 나타나며 그것도 Blend 3로만 연결되어 실행되는 결과가 나타납니다.
이 문제를 해결하기 위해서는 레지스트리를 수정해서 해결 할 수 있습니다.

레지스트리 편집기에서 HKLM\Software\Microsoft\Expression\Blend 항목에서 VS키에서 String 이름을BlendLaunchPath 로 해주고 Expression Blend의 경로를 입력해주면 됩니다.

 1: [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Expression\Blend\VS]
 2: "BlendLaunchPath"="\"c:\\Program Files\\Microsoft Expression\\
     Blend Preview for .NET 4\\Blend.exe\""

x64 시스템의 경우 경로가 조금 다릅니다.
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Expression\Blend\VS

Wow6432Node 아래에 있으니 주의하시고 수정하시기 바랍니다.

수정하기 번거로우면 첨부 파일을 열어서 시스템에 맞는 파일로 실행시켜주면 됩니다.



※ 원문은 Microsoft의 Tim Heuer 블로그에 있습니다.
신고