- 뜬금없이 뭐여..?

지금까지는 닷넷 4.0에 추가된 TPL과 PLINQ를 통해서 멀티 스레드 프로그래밍을 하는 방법을 살펴봤습니다. 그러면, 잠깐 추억을 되살릴겸, 뭐가 어떻게 달라졌는지도 한번 비교해 볼겸 해서, 닷넷 3.5까지의 멀티 스레드 프로그래밍 방법을 잠깐 살펴보도록 하겠습니다. 호호호호


- Thread와 다이다이로 작업하던 시절.

TPL은 System.Threading.Tasks를 사용해서, ThreadPool을 내부적으로 사용한다고 말씀을 드렸었습니다. 하지만, 그것 닷넷 4.0이나, 닷넷 3.5에서는 Reactive Extension(Rx)을 통해서 추가적으로 지원하는 기능이구요. 그 이전에는 직접적으로 Thread나 ThreadPool을 이용해서 프로그래밍 해야 했습니다. 그럼 Thread를 직접 사용하던 코드를 예제로 한번 보시죠.

using System;
using System.Threading;

namespace Exam18
{
    class Program
    {
        static readonly int max = 10000;

        public static void PrintAsync()
        {
            for (int count = 0; count < max; count++)
            {
                Console.Write("|");
            }
            Console.WriteLine("추가 스레드 끝");
        }

        static void Main(string[] args)
        {
            ThreadStart threadStart = PrintAsync;
            Thread thread = new Thread(threadStart);

            //추가 스레드 시작
            thread.Start();

            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }
            Console.WriteLine("메인 스레드 끝");

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            thread.Join();
        }
    }
}

<코드1> Thread와 다이다이로.

<코드1>을 보면, 맨 처음에 Task를 소개해드리면서 사용했던 예제를 Thread를 사용하도록 바꾼 코드입니다. 차이점이 있다면, ThreadStart타입의 델리게이트를 사용해야 한다는 것과, Wait()메서드가 아니라 Join()메서드를 사용한다는 것이죠. 결과를 보시면, Task를 사용했던 것과 동일합니다.

---------|||||||-|||||||||||--------------|||||||||-------------|||||------|||||||||||---------
-||||||||--------|||||||||||||-----메인 스레드 끝||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||추가 스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과1> Thread를 사용한 결과.

그리고 Thread를 보면, Task와 마찬가지로 실행을 제어할 수 있도록 몇가지 속성을 제공하는데요, 그 목록을 보면 아래와 같습니다.

 속성  설명 
 Join()  추가 스레드가 완료되기 전에 메인 스레드가 완료되면, 추가 스레드가 하던 작업은 다 날아간다. 그래서 추가 스레드의 작업이 완료될 때까지 메인 스레드가 기다리도록 한다.
 IsBackground  이 속성은 기본적으로 false이다. 즉, 스레드는 기본적으로 foreground작업인데, 그 때문에 스레드가 완료되기 전까지는 프로세스를 종료시킬 수 없다. 이 속성을 true로 주면, 스레드의 작업이 완료되기 전에도 프로세스를 종료시킬 수 있다.
 Priority  Join메서드를 사용한 경우에, 이 속성을 통해서 스레드의 우선순위를 바꿀 수 있다.
 ThreadState  이 속성을 통해서 스레드의 상태를 파악할 수 있는데, Aborted, AbortRequested, Background, Runnging, Stopped, StopRequested, Suspended, SuspendRequested, Unstarted, WaitSleepJoin등의 상태 값을 얻을 수 있다.
 Thread.Sleep()  현재 실행 중인 스레드의 실행을 명시한 시간만큼 일시정시 시키는 메서드이다.
 Abort()  이름 그대로, 스레드를 중지시키는 메서드. ThreadAbortException이 발생된다.
<표1> Thread의 속성.

위의 Thread멤버 중에서, Task에도 있는 건, Join()과 ThreadState뿐입니다. 왜 그럴까요? 일반적으로 권장되지 않는 것들이기 때문이죠. 그래서 닷넷 프레임워크 4.0으로 프로그래밍 할 때는, 위에서 언급한 것들 중에서 Task에 없는 속성들을 될 수 있으면 사용하지 말아야 합니다.


- ThreadPool을 사용해보자.

ThreadPool을 사용하면, 새로운 스레드를 계속 해서 생성하기 보다 기존에 있는 스레드를 재활용해서 추가적인 스레드 생성을 막을 수 있습니다. 참고로, TPL이 내부적으로 ThreadPool을 사용한다고 말씀드렸었죠? 그럼 ThreadPool을 사용하는 예제도 한번 보시죠.

using System;
using System.Threading;

namespace Exam19
{
    class Program
    {
        static readonly int max = 10000;

        public static void PrintAsync(object state)
        {
            for (int count = 0; count < max; count++)
            {
                Console.Write(state.ToString());
            }
            Console.WriteLine("추가 스레드 끝");
        }

        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(PrintAsync, "|");
           
            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }
            Console.WriteLine("메인 스레드 끝");

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            Thread.Sleep(1000);
        }
    }
}

<코드2> ThreadPool을 사용한 코드.

<코드2>를 보시면, ThreadPool을 사용하고 있는데요. QueueUserWorkItem메서드를 통해서 작업을 추가하고 있습니다. 그러면, 자동으로 스레드를 활용해서 작업을 시작하게 되구요. 결과는 앞선 예제와 동일합니다. 그런데, ThreadPool을 사용할 때 장점만이 있는 건 아닌데요. 작성한 코드외에도 다른 라이브러리등에서 내부적으로 시간이 많이 걸리는 I/O작업 등에 ThreadPool을 사용한다면, 그 작업이 끝날 때까지 기다려야 하거나, 심한 경우에는 데드락이 발생하기도 합니다. 그리고 Thread나 Task를 사용할 때와는 다르게 ThreadPool은 실행 중인 작업에 접근할 수 있는 방법이 없습니다. 그래서 실행 중인 작업을 조종한다거나, 상태를 확인할 수 가 없죠. 그래서 <코드2>를 보시면, Join()이나 Wait()대신에, Thread.Sleep()메서드를 통해서 추가 스레드가 끝날 때까지 메인 스레드를 기다리게 합니다.


- 마치면서

오늘은 닷넷 3.5 까지의 멀티 스레드 프로그래밍 방법에 대해서 알아봤는데요. 크게 다른 모습은 없습니다. 다만, 좀 더 안전하고 간단한 방법을 제공하는 것이죠. 대한민국도 16강에 진출했는데 오늘은 여기까지 하시죠!...응??


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
- 이건 또 무슨 신개념 속담 드립인거.

저는 늘 의문을 품어왔습니다...는 훼이크고 이번 포스트를 준비하면서 의문을 가지게 되었습니다. 분명 병렬 프로그래밍의 정신은 남아도는 코어를 활용해서 협력을 해서 작업을 좀 더 빨리 끝내자고 하는 건데요, 그런면에서 '백지장도 맞들면 낫다'는 말은 병렬 프로그래밍의 정신을 잘 표현하는 선조들의 지혜라고 볼 수 있습니다. 그런데요.... 과연 백지장같이 갓난 아기도 혼자들 수 있는 걸 같이 드는게 과연 나은 일일까요? 오히려 혼자 할 때보다 못한 결과를 가져오지는 않을까요? 오늘은 그에 대한 이야기입니다.


- LINQ도 맞들면 낫다, 어헣.

LINQ는 데이터 쿼리에 가까운 표현을 사용하면서, 데이터 쿼리시에 직관적이고 선언적인 코드를 활용할 수 있도록 해주었는데요. 거기에 이전 포스트들에서 설명드렸던 Parallel.For나 Parallel.ForEach처럼 매우 간단하게 남아도는 코어를 활용할 수 있도록 하는 방법을 제공합니다.

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

namespace Exam15
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Func<int, int> square = (num) => {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };

            nums = nums.AsParallel()
                .Select(square)
                .ToArray();
        }
    }
}

<코드1> LINQ를 맞드는 예제.

<코드1>을 보시면, 1부터 1000까지의 숫자를 가진 배열을 생성하고, 각 수를 제곱한 수를 구하는 코드입니다. 기존의 LINQ코드와 다른 점이 있다면, 제곱 연산을 수행하기 위한 데이터 소스인 nums에 대해서 AsParallel()을 호출했다는 것입니다. <코드1>에선 AsParallel()의 리턴타입이 ParallelQuery<int>인데요, LINQ에서는 Enumerable을 사용하지만, PLINQ에서는 ParallelEnumerable을 사용합니다.

<코드1>을 보면, 정말 간단하게 병렬 프로그래밍이 구현되는데요. 정말 저렇게 간단한 방법으로 병렬 쿼리가 실행되는지 확인하기 위해서 Task.CurrentId를 통해서 실행중인 스레드의 Id를 출력하도록 했습니다. 그리고 비교적 일관성 있는 결과를 얻기 위해서 Thread.Sleep를 통해서 실행을 조금 여유롭게 해줬죠. 결과를 보실까요?

(생략)
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
3
1
4
2
계속하려면 아무 키나 누르십시오 . . .
<결과1> LINQ를 맞든 결과.

3->1->4->2의 패턴이 반복되는 걸 확인하실 수 있습니다. 물론, 실행도중에 패턴은 바뀌기도 합니다만, 분명 AsParallel()메서드를 호출하는 것 만으로도 병렬 프로그래밍이 구현된 것이죠. 그런데, 출력되는 스레드의 아이디를 보면, 딱 4개만 생성된 걸 확인할 수 있는데요. 제 컴퓨터의 CPU가 쿼드코어라서 딱 4개만 생성된 것 같습니다. 그런데 왜 딱 4개만 생성된 걸까요? 이전에 TPL을 활용해서 작업할 때는 4개 이상의 스레드도 생성되어서 작업을 처리했는데 말이죠. 그건 PLINQ가 병렬 쿼리를 처리하는 방식에서 원인을 찾을 수 있습니다.

제가 술을 먹고 만취한 상태에서 글을 적어서 그럴까요? 아래 내용은 새빨간 거짓말 입니다!!! 낄낄낄-_-;;; 스레드가 4개만 생성된 건, PLINQ가 분할 알고리즘으로 구간 분할을 사용하기 때문에 그렇습니다. 그리고 정확한 설명은, PLINQ는 ParallelEnumerable타입 같이 병렬 쿼리를 돌려도 안전한 타입에 대해서는 주저없이 쿼리를 병렬화 해서 작업을 하지만, IEnumerable타입 같이 병렬로 쿼리를 돌릴 때, 안전하다고 보장할 수 없는 경우에는 순차적인 쿼리로(정확히 말하지만, 순차적인 쿼리가 아니라 Chunk 분할 알고리즘을 통해서 데이터 소스에 락을 걸고, 스레드가 한번에 작업할 덩어리를 떼어주는 형태로)작업을 하게 됩니다. 오해 없으시길 바랍니다! 어헣-_-;;;

PLINQ는 AsParallel()메서드로 데이터 소스에 대해서 병렬처리를 원했다고 하더라도 항상 병렬적으로 처리를 하지는 않습니다. 예를 들면, 작업이 너무나 간단해서, 병렬적으로 처리할 때 오히려 손해를 보는경우가 있습니다. 작업이 너무 간단하기 때문에 각 스레드가 처리하는 작업의 시간이 매우 짧고, 그래서 작업 처리에 걸리는 시간보다, 스레드 간의 작업전환에 더 많은 시간이 걸리는 것이죠. 그래서 PLINQ는 AsParallel()이 호출되면, 우선 쿼리를 분석합니다. 그리고 그 쿼리가 간단하다는 판단을 하면, 병렬적으로 처리하기 보다는 순차적으로 처리를 하게 되는 것이죠. <결과1>에서 스레드가 4개가 돌아간 것은, CPU의 코어가 4개 이기 때문에, 코어별로 스레드가 한 개씩 생성된 것입니다. 각 코어의 입장에서 보자면, 스레드가 한 개씩 있는 셈이므로 작업전환이 필요없겠죠. 참고로, 듀얼 코어인 제 노트북에서 실행한 결과는 아래와 같습니다.

(생략)
1
2
1
2
1
1
2
1
2
1
2
2
1
2
1
2
1
2
1
2
1
계속하려면 아무 키나 누르십시오 . . .
<결과2> 듀얼 코어에서 맞든 LIINQ 결과.

그런가 하면, 몇 개의 스레드를 생성할 것인지 명시해 줄 수도 있는데요. <코드2>와 같이 3개의 스레드를 명시해주고 결과를 보겠습니다.

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

namespace Exam16
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Func<int, int> square = (num) =>
            {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };

            nums = nums.AsParallel()
                .WithDegreeOfParallelism(3)               
                .Select(square)
                .ToArray();
        }
    }
}

<코드2> LINQ를 맞들 스레드의 개수를 지정하는 코드.

(생략)
1
3
2
3
1
2
1
3
2
1
3
2
1
3
2
1
3
2
3
1
2
3
계속하려면 아무 키나 누르십시오 . . .
<결과3> 3개의 스레드로 맞든 LINQ 결과.

패턴은 약간 불안정할 때도 있지만, 대략 1->2->3의 순서를 유지하고 있습니다. 그런데, 왜 이렇게 스레드의 개수를 정해 줄 수도 있게 했을까요? 바로 최적화 때문입니다. 기본적으로 PLINQ의 알고리즘은 많은 경우를 테스트해서 최적화 알고리즘을 만들어 놓았기 때문에, 대부분의 경우는 기본옵션으로 실행하는 것이 가장 좋은 결과를 냅니다. 하지만, 그렇지 못한 경우가 있을 수 있는데요. 그럴 때, 테스트를 통해서 적절한 스레드 개수를 지정할 수 있도록 옵션을 둔 것이죠.

위에서 쿼리 식이 단순하면, 순차적으로 실행한다고 말씀을 드렸는데요, 쿼리 식이 병렬로 실행하기에 안전하지 못한 경우에, 순차적으로 실행하다고 말씀을 드렸는데요, 그런 경우도 병렬적으로 실행을 강제할 수 있습니다. 쿼리 식에 '.WithExecutionMode(ParallelExecutionMode.ForceParallelism)'메서드를 추가하면, 기본 알고리즘과는 상관없이 무조건 병렬적으로 실행하도록 합니다. 실행시간을 테스트한다거나 할때 유용하게 사용할 수 있는 옵션이겠죠.


- LINQ 맞들기 취소는 어떠케?

이번에는 PLINQ 쿼리를 취소하는 방법에 대해서 알아보겠습니다. 지금까지 취소에는 CancellationTokenSource를 활용했었죠? 마찬가지 입니다. 똑같이 Token을 활용해서 취소에 사용하되, 사용하는 방법이 조금씩 다른 것 뿐이지요.

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

namespace Exam17
{
    class Program
    {
        public static int[] SimpleParallelTask(int[] source,
            CancellationToken token)
        {
            Func<int, int> square = (num) =>
            {
                Console.WriteLine(Task.CurrentId);
                Thread.Sleep(10);
                return num * num;
            };
           
            return source.AsParallel()
                .WithCancellation(token)
                .WithDegreeOfParallelism(3)
                .Select(square)
                .ToArray();
        }

        static void Main(string[] args)
        {
            CancellationTokenSource cts =
                new CancellationTokenSource();

            Console.WriteLine("끝내려면 아무키나 누르세요");

            int[] nums = Enumerable.Range(1, 10000).ToArray();

            Task task = Task.Factory.StartNew(() =>
                {
                    nums = SimpleParallelTask(nums, cts.Token);
                }, cts.Token);

            Console.Read();

            cts.Cancel();
            Console.WriteLine("-------------------------------------");

            try
            {
                task.Wait();
            }
            catch (AggregateException)
            {
                Console.WriteLine("쿼리가 중간에 취소되었습니다.");
            }
        }
    }
}

<코드3> LINQ 맞들기 취소하기.

<코드3>을 보면, AsParallel메서드의 결과로 리턴되는 ParallelQuery타입에 포함된 .WithCancellation메서드를 사용해서 PLINQ 쿼리에 CancellationToken을 넘겨준다는 것을 제외하고는 Parallel.For, Parallel.ForEach와 동일한 방법을 사용하고 있습니다. 결과도 예측할 수 있듯이 동일합니다.

(생략)
1
3
2
1
3
2
1
3
2
1
3
2
1
3
2
1
3

-------------------------------------
쿼리가 중간에 취소되었습니다.
계속하려면 아무 키나 누르십시오 . . .

<결과4> LINQ 맞들기를 취소한 결과.


- 마치면서

어떠셨나요? '백지장도 맞들면 낫다'는 속담이 PLINQ에서는 항상 참이 아니라는 게 말이죠. 이래서 병렬 프로그래밍이 어려운가 봅니다. 어허허허헣. 악플 사절(죽을때 까지 미워할거임)! 피드백 환영! 호호호호^^


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 취소는 어렵지.

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


- 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

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 뭐, 미안하다고?

선진국에 가보면, 약간만 부딛힐 듯한 상황이라면, 서로 '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

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- To be continue??

한 때, 뮤직비디오를 드라마 처럼 만드는 게 유행했던 때가 있었습니다. 기억하시나요? 한창 조성모가 데뷔했을 때로 기억하는데요. 노래 한곡에 뮤직비디오가 여러편이 있어서, 하나가 공개 되고 나면, 'To be continue..'라는 자막이 나오면서 애간장을 태우곤 했었죠. 그게 오늘 내용과 무슨 상관이냐!?!? 별 상관없습니다-_- 어허허헣.


- 뒤를 부탁해, ContinueWith

지금까지는 작업을 만들어서 돌리고, 작업이 종료되면 그냥 끝났었는데요. 한 작업이 어떤 상태로 끝났을 때, 그 상태에 따라서 다른 작업이 연쇄적으로 일어나도록 설정하고 싶은 경우가 있겠죠. Task클래스에는 그런 경우를 위해서 ContinueWith라는 메서드가 준비되어 있습니다. 낄낄낄. 이 메서드는 선행 작업(antecedent task)에 하나 이상의 연쇄적인 작업을 연결 시켜놓고, 선행 작업의 상태나 종료에 따라서 연쇄적으로 이후 작업이 실행되도록 설정할 수 있도록 해줍니다. 그러면, 선행 작업의 상태에 대해서 설정가능한 상태가 뭐가 있는지 한번 확인해볼까요?

 상태  설명 
 None  아무 값도 명시되지 않으면 사용되는 기본 값으로, 선행작업의 상태와 관계없이, 선행작업이 종료되면 바로 시작된다.
 NotOnRanToCompletion  선행작업이 끝난 상태에서는 연쇄작업을 실행계획에 넣지 않는다.
 NotOnFaulted  선행작업이 처리안된 예외를 던지면, 연쇄작업을 실행계획에 넣지 않는다.
 NotOnCanceled  선행작업이 취소되면, 연쇄작업을 실행계획에 넣지 않는다.
 OnlyOnRanToCompletion  선행작업이 정상적으로 완료된 상태에서만 연쇄작업을 실행계획에 추가시킨다.
 OnlyOnFaulted  선행작업이 처리안된 예외를 던지는 상태에서만 연쇄작업을 실행계획에 추가시킨다.
 OnlyOnCanceled  선행작업이 취소된 상태에서만 연쇄작업을 실행계획에 추가시킨다.
<표1> http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx에서 참조한 상태값과 설명

그리고 ContinueWith메서드는 Task타입의 객체를 리턴하는 데요, 그 덕분에 연쇄적으로 작업에 작업을 계속해서 추가할 수도 있습니다. 그러면, 예제를 한번 확인해볼까요?

using System;
using System.Threading.Tasks;

namespace Exam9
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if(i == 100000 || i == 200000)
                {
                    //100000에 이르면, 그냥 실행을 종료시킨다.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            task.Start();

            Task<string> justDoIt = task.ContinueWith<string>(
                (antecedentTask) =>
                {
                    Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
                    return Calc(100001L);
                },
                TaskContinuationOptions.None);

            Task<string> justDoIt2 = justDoIt.ContinueWith<string>(
                (antecedentTask) =>
                {
                    Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
                    return Calc(200001L);
                },
                TaskContinuationOptions.None);

            try
            {
                Console.WriteLine(task.Result);
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }
            finally
            {
                try
                {
                    Console.WriteLine(justDoIt.Result);
                }
                catch (AggregateException ex)
                {
                    foreach (var item in ex.InnerExceptions)
                    {
                        Console.WriteLine("에러 : {0}", item.Message);
                    }
                }
                finally
                {
                    Console.WriteLine(justDoIt2.Result);
                    Console.WriteLine("끝");
                }
            }
        }
    }
}

<코드1> 연쇄작업 물리기.

<코드1>은 Calc메서드를 실행하는 작업을 하나 시작하고, 그 작업에 연쇄작업을 하나 연결하고, 그 연쇄작업에 또 연쇄작업을 하나 연결한 예제입니다. 연쇄작업의 옵션을 보면, 전부 'TaskContinuationOptions.None'로 되어 있는데요. <표1>에서 확인해보면, 선행작업이 어떤 식으로 종료되든, 종료가 되면 이어서 실행하도록 하는 옵션이죠. 그리고 Calc메서드를 보면, for루프의 카운트가 100000과 200000에 이르면, 예외를 던지고 실행을 종료하도록 했습니다. 그리고 작업을 보면, task는 1부터, justDoIt은 100001부터 실행하므로 둘다 예외를 만나겠죠. 그리고 마지막 justDoIt2는 200001부터 실행하므로 실행을 종요하게 됩니다. 그럼 결과를 보죠.(설명을 위한 예제이므로 실제로 이렇게 짜면 곤란하겠죠)

현재 이 메서드를 실행중인 스레드 ID : 1
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 2
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 3
에러 : 그냥 에러가 났음
에러 : 그냥 에러가 났음
계산 끝
4999979949900000

계속하려면 아무 키나 누르십시오 . . .
<결과1> 연쇄작업 실행 결과.

우리가 예측한대로, 선행작업이 실패하면서, 바로 다음작업이 연쇄적으로 실행되는 걸 확인할 수 있습니다. 지난 포스트에서 Wait메서드를 통해서 추가적으로 생성된 스레드의 결과를 기다리거나, Result속성을 통해서 결과값을 요청하지 않으면 추가스레드의 결과는 그냥 무시된다고 했었는데요. 연쇄작업도 마찬가지 입니다. 연쇄작업을 연결해 놓았다고 해도, Wait로 기다리거나 Result를 요구하지 않으면 그냥 무시되어 버리는 것이죠. <코드1>에서 justDoIt2.Result부분을 아래 처럼 주석처리하고 실행해보죠.

finally
{
    //Console.WriteLine(justDoIt2.Result);
    Console.WriteLine("끝");
}
<코드2> 주석 처리.

그리고 결과는 보면,

현재 이 메서드를 실행중인 스레드 ID : 1
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 2
이전 작업 상태 : Faulted
현재 이 메서드를 실행중인 스레드 ID : 3
에러 : 그냥 에러가 났음
에러 : 그냥 에러가 났음

계속하려면 아무 키나 누르십시오 . . .
<결과2> 작업이 무시된 결과

justDoIt2가 진행하던 작업의 결과는 무시된 걸 확인할 수 있습니다.

<표1>에서 None옵션은 선행작업이 어떤 이유로 종료가 되었든, 종료가 되었다면 바로 이어서 연쇄작업을 실행하도록 하는 옵션인데요. 그러면, <코드1>과 동일한 결과를 가져오려면 어떤 옵션을 사용하면 되는 걸까요? 한번 생각해보시죠. 어헣. 힌트는 예외를 던지고 종료되는 작업의 상태를 보시면 바로 정답이 나옵니당. 정답은 <코드3>을 보시죠!

Task<string> justDoIt = task.ContinueWith<string>(
    (antecedentTask) =>
    {
        Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
        return Calc(100001L);
    },
    TaskContinuationOptions.OnlyOnFaulted);

Task<string> justDoIt2 = justDoIt.ContinueWith<string>(
    (antecedentTask) =>
    {
        Console.WriteLine("이전 작업 상태 : {0}", antecedentTask.Status);
        return Calc(200001L);
    },
    TaskContinuationOptions.OnlyOnFaulted);

<코드3> 같은 결과를 가져오는 코드

그렇씁니다. OnlyOnFaulted는 선행작업이 처리안된 예외를 던지는 상태에서만 연쇄작업을 실행시키는 옵션이죠. 어허허허헣.

그렇다면, 작업의 성공이나 실패를 자동으로 통지하도록 하는 방법을 사용할 수 있을 것 같습니다.

using System;
using System.Threading.Tasks;

namespace Exam10
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if (i == 100000)
                {
                    //100000에 이르면, 그냥 실행을 종료시킨다.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            task.Start();

            Task completedTask = task.ContinueWith(
                (antecedentTask) =>
                {
                    Console.WriteLine("Task State: 무사 완료!!");
                    //작업 완료 후에 이어서 뭔가를 처리
                },
                TaskContinuationOptions.OnlyOnRanToCompletion);

            Task faultedTask = task.ContinueWith(
                (antecedentTask) =>
                {
                    Console.WriteLine("Task State: 처리되지 않은 예외 발견!!");
                },
                TaskContinuationOptions.OnlyOnFaulted);

            try
            {
                completedTask.Wait();
                faultedTask.Wait();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }
        }
    }
}

<코드4> 연쇄 작업으로 상태를 감지

<코드4>를 보시면, 작업이 무사히 끝났을 때와 처리안된 예외가 생겨서 종료될 때에 실행되도록 연쇄작업을 두개 연결해 놓았습니다. 일단 결과를 보면요,

현재 이 메서드를 실행중인 스레드 ID : 1
Task State: 처리되지 않은 예외 발견!!
에러 : 작업이 취소되었습니다.
계속하려면 아무 키나 누르십시오 . . .
<결과3>

예상대로, OnlyOnFaulted로 설정된 연쇄작업이 실행된 것을 확인할 수 있습니다. 왜냐면 작업을 1부터 시작하도록 했기 때문에, 카운트가 100000에 다다르면, 처리안된 예외가 발생하기 때문이죠. 작업의 상태값을 100001L이상으로 설정하면, OnlyOnRanToCompletion으로 설정된 연쇄작업이 실행되겠죠.

그런데, 두 경우다 예외가 하나씩 잡히는 걸 확인할 수 있습니다. '작업이 취소되었습니다'라는 예외인데요. 이유는 간단합니다. task에 OnlyOnRanToCompletion와 OnlyOnFaulted인 연쇄작업 두개를 연결했기 때문에, 둘 중 하나만 항상 실행이 됩니다. 그래서 둘 중에 하나는 실행이 되지 못한채, 선행작업이 끝나버리는 거죠. 그래서 Wait메서드를 통해서 기다리고 있던 작업은 종료되지 못하고 취소가 되는 마는 것이죠.


- 마무리

오늘은 선행작업의 상태에 따라서 연쇄적으로 작업을 실행하는 예제를 살펴봤습니다. 다음에 뵙쬬!


- 참고자료

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

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 인생이 원하는 대로 가지는 않더라.

우리는 인생을 살면서, 여러가지 계획을 세웁니다. 하지만, 보통 계획을 세울 때, 예외적인 상황을 감안하지 않는 경우가 많습니다. 그래서 늘 일정은 실패로 끝나게 되고, '아, 난 안되나 보다.'하고 절망하게 되는 것이죠. 자세한 내용은 최인철 교수님의 '프레임'을 참고하시면..... 순간 책 소개 코너로 빠져들뻔 했군요. 어헣.

아무튼, 우리는 인생에서 뿐만 아니라 프로그래밍에서도 생각외의 순간을 많이 만나게 됩니다. '도대체 어떤 **가 이런 거 까지 해볼까?'하는 생각으로 안이하게 프로그래밍을 하다보면 기상 천외한 버그 리포트를 받게 됩니다. 그래서 예외 처리가 중요한 것이죠. 오늘은 병렬 프로그래밍에서 예외 처리 하는 법에 대해서 간단하게 이야기를 해보려고 합니다.


- 차이점 하나.

기존의 프로그래밍에서는 그저 예외를 발생시 처리하고 싶은 구문을 try로 감싸면 되었는데요. 병렬 프로그래밍에서는 어떨까요? Task.Start()를 try로 감싸면 결과를 얻을 수 있을까요? 한번 실험해보죠.

using System;
using System.Threading.Tasks;

namespace Exam5
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
                if (i == 100000)
                {
                    //100000에 이르면, 그냥 예외를 던진다ㅋ.
                    throw new ApplicationException("그냥 에러가 났음");
                }
            }
            Console.WriteLine("계산 끝");
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(
                Calc, 1L);

            try
            {
                task.Start();
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("에러 : {0}", item.Message);
                }
            }

            Console.WriteLine(task.Result);
        }
    }
}

<코드1> Start를 try로 감싸기

<코드1>을 보시면, task.Start()를 try로 감싸고 있습니다. 과연 실행중에 던지는 예외를 잘 받을 수 있을까요? 결과는 아래와 같습니다.

현재 이 메서드를 실행중인 스레드 ID : 1

처리되지 않은 예외: System.AggregateException: 하나 이상의 오류가 발생했습니다.
---> System.ApplicationException: 그냥 에러가 났음
   위치: Exam5.Program.Calc(Object from) 파일 C:\Users\boram\Documents\Visual St
udio 10\Projects\Chapter9\Exam7\Program.cs:줄 21
   위치: System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)
   위치: System.Threading.Tasks.Task.InnerInvoke()
   위치: System.Threading.Tasks.Task.Execute()
   --- 내부 예외 스택 추적의 끝 ---
   위치: System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCance
ledExceptions)
   위치: System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, Cancellatio
nToken cancellationToken)
   위치: System.Threading.Tasks.Task`1.get_Result()
   위치: Exam5.Program.Main(String[] args) 파일 C:\Users\boram\Documents\Visual
Studio 10\Projects\Chapter9\Exam7\Program.cs:줄 45
계속하려면 아무 키나 누르십시오 . . .

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

<결과1>을 보면, 예외는 전혀 잡히지 않았습니다. 왜 일까요? 작업에서 실행하는 코드(여기서는 Calc메서드)는 Start메서드 내에서 실행되는 게 아니라, Start메서드로 작업이 시작된 이후에야 별도로 시작되기 때문이죠. 그래서 Start메서드에 try를 걸어봤자 예외는 잡을 수 없습니다.

가만히 생각해보면, 작업 안에서 처리되는 예외(즉, Calc메서드 내에서 발생하고 처리되는 예외)는 전혀 고민할 필요가 없겠죠. 하지만, 처리되지 못한 예외가 발생하는 경우는 바깥에서 처리할 방법을 찾아야 합니다.

CLR 2.0 버전까지는 이런 처리되지 못한 예외가 발생하면, 예외가 버블링되면서, 상위 계층으로 전파되면서 결국에는 윈도우 에러 보고 대화상자를 열게 만들고, 프로그램은 종료되었습니다. 하지만, 작업 내부에서 처리되지 못한 예외라고 하더라도, 작업 바깥에서 처리할 수 있는 방법이 있다면, 프로그램이 종료되는 것 보다는 바깥에서 처리할 수 있게 하는 게 더 나은 방법이겠죠.

작업내부에서 처리 안된 예외(unhandled exception)가 발생했다면, 그 예외는 일단 작업을 마무리 짓는 멤버(Wait(), Result, Task.WaitAll(), Task.WaitAny())가 호출되기 전까지는 조용히 기다립니다. 그리고 마무리 멤버의 호출에서 처리 안된 예외가 발생하게 되는 것이죠. <코드1>에서 Start메서드를 try블록으로 감쌌지만, 예외를 잡을 수 없었던 이유가 바로 여기에 있습니다. 처리 안된 예외는 마무리 멤버와 함께 발생하기 때문이죠. 그러면, <코드1>을 수정해서, 예외를 제대로 잡도록 수정해보겠습니다.

task.Start();

try
{
    Console.WriteLine(task.Result);
}

catch (AggregateException ex)
{
    foreach (var item in ex.InnerExceptions)
    {
        Console.WriteLine("에러 : {0}", item.Message);
    }
}

<코드2> Result를 try로 감싸라.

<코드2>를 보면, 작업을 마무리 짓는 멤버 중의 하나인, Result를 try블록으로 감싸고 있습니다. 그리고 결과를 보면,

현재 이 메서드를 실행중인 스레드 ID : 1
에러 : 그냥 에러가 났음
계속하려면 아무 키나 누르십시오 . . .
<결과2> 제대로 처리된 예외.

예외가 제대로 처리 된 것을 확인할 수 있습니다.


- 차이점 둘.

혹시 <코드1>, <코드2>를 주의 깊게 보신 분이라면, 처음보는 예외 하나를 발견하셨을지도 모르겠습니다. 바로 AggregateException인데요, 이에 대해서 이야기를 좀 해보겠습니다.

AggregateException은 닷넷 프레임워크 4.0에서 처음 추가된 예외 타입인데요, MSDN의 설명을 보면(http://msdn.microsoft.com/en-us/library/system.aggregateexception.aspx), '프로그램 실행 중에 발생하는 하나 또는 여러개의 에러를 표현하는 수단'이며, 주로 TPL과 PLINQ에서 활용되고 있다고 합니다.

aggregate는 여러 개의 작은 것들을 서로 합치는 이미지를 갖고 있는데요, AggregateException은 그렇다면 여러 개의 예외를 하나로 합치는 예외타입이라는 말이 됩니다. 그런데, 여러 개의 예외는 어디서 오는 걸까요? 예전에 작성했던 예제를 조금 수정해서 확인 해보도록 하죠.

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

namespace Exam7
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

            Console.WriteLine("파일 개수 : {0}", files.Count());
           
            Parallel.ForEach(files, (file) =>
            {
                FileInfo fileInfo = new FileInfo(file);
                if (fileInfo.Exists)
                {
                    if (fileInfo.Length >= 15000000)
                    {
                        throw new ApplicationException("15메가 넘는 파일이!!");
                    }
                    else if (fileInfo.Length >= 1000000)
                    {
                        fileList.Add(fileInfo.Name);
                    }
                    Console.Write("{0}", Task.CurrentId.ToString());                   
                }
            });
        }
    }
}

<코드3> 약간 수정된 예제.

<코드3>이 바로 그 예제인데요, 예제를 보면, 음악 폴더에서 15메가 넘는 파일이 발견되면, 예외를 발생하도록 되어있습니다. flac같은 파일로 보자면, 15메가 넘는 파일은 흔히 있겠지만, 저는 서민이라 mp3를 선호합니다. 어헣-_-. 아무튼, 이 예제를 실행 시켜보면요, 간혹 15메가 넘는 파일이 몇개는 있기 마련이기 때문에, 실행 중에 에러가 나게 되어 있습니다. 한번 디버깅을 해보죠.


<그림1> 병렬 스택 창

<그림1>의 병렬 스택을 보시면요, 병렬 ForEach문에 의해서 4개의 작업자 스레드가 생성된 걸 확인할 수 있습니다.(리스트의 크기나, CPU자원 상태등에 따라서 개수는 계속해서 변합니다.)

<그림2> 병렬 작업 창

그리고 <그림2>의 병렬 작업창을 보면, 2번 스레드가 15메가 넘는 파일을 만난 것을 확인할 수 있습니다. 그러면, 2번 스레드가 예외를 던질텐데, 나머지 작업은 어떻게 될까요? 처리 되지 않은 예외가 발생하는 순간, 다른 작업들은 모두 날아가버립니다.

그런데, 각 스레드 별로 파일 리스트를 쪼개서 줬을 텐데요. 각 스레드가 각자의 목록을 가지고 작업을 하다보면, 각 스레드 별로 15메가가 넘는 파일을 발견하게 될 것입니다. 이런 예외들을 만날 때 마다 처리하지 말고, 모두 모아서 한번에 처리하려면 어떻게 해야 할까요? 그래서 바로 AggregateException을 사용하는 거죠.

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

namespace Exam8
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable<string> files =
                Directory.GetFiles("C:\\음악", "*", SearchOption.AllDirectories);
            List<string> fileList = new List<string>();

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

            var exceptions = new ConcurrentQueue<Exception>();

            try
            {
                Parallel.ForEach(files, (file) =>
                {
                    FileInfo fileInfo = new FileInfo(file);
                    if (fileInfo.Exists)
                    {
                        try
                        {
                            if (fileInfo.Length >= 15000000)
                            {
                                throw new ApplicationException("15메가 넘는 파일이!!");
                            }
                            else if (fileInfo.Length >= 1000000)
                            {
                                fileList.Add(fileInfo.Name);
                            }
                            Console.Write("{0}", Task.CurrentId.ToString());
                        }
                        catch (Exception ex)
                        {
                            exceptions.Enqueue(ex);
                        }
                    }
                });

                throw new AggregateException(exceptions);
            }
            catch (AggregateException ex)
            {
                foreach (var item in ex.InnerExceptions)
                {
                    Console.WriteLine("\n에러 : {0}", item.Message);
                }

                Console.Write("\n파일 리스트 계속해서 보기(엔터키를 치세요)");
                Console.ReadLine();
            }
            finally
            {
                foreach (string file in fileList)
                {
                    Console.WriteLine(file);
                }

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

<코드4> AggregateException을 사용.

ConcurrentQueue는 Queue긴 하지만, 여러 스레드에 의해서 동시에 큐에 추가를 하거나 해도 안전하도록 만들어진(thread-safe) Queue입니다. 그 큐에서 예외가 발생할 때마다, 예외를 저장해뒀다가, 한꺼번에 AggregateException으로 던지는 것이죠. 그리고 바깥쪽의 catch블록에서 예외를 받아서 내부의 예외 목록을 하나씩 처리하는 것입니다.

(생략)
에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

에러 : 15메가 넘는 파일이!!

파일 리스트 계속해서 보기(엔터키를 치세요)

<결과 3> 처리과정에서 생긴 예외를 모두 모아서 처리.


- 마무리.

오늘 예외처리에 대해서 봤습니다. 다음은~? 작업을 연쇄적으로 처리할 수 있도록 하는 부분을 보겠습니돠. 그럼 평안하시길. 어허허허허헣.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd460695.aspx
3. http://msdn.microsoft.com/en-us/magazine/ee321571.aspx
4. http://msdn.microsoft.com/en-us/library/system.aggregateexception.aspx

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 잡설없이 바로 고고고!

지난 포스트의 마지막 예제에서 Task 클래스의 IsCompleted라는 속성을 사용했었습니다. 이름 그대로, 해당 작업이 끝났는지를 확인할 수 있는 속성인거죠. 이런 속성말고는 또 뭐가 있을까요? 몇 가지 쓸모 있는 속성을 들을 예제를 통해서 확인해보도록 하죠.

using System;
using System.Threading.Tasks;

namespace Exam4
{
    class Program
    {
        static string Calc(object from)
        {
            long sum = 0;
            long start = (long)from;
            Console.WriteLine("현재 이 메서드를 실행중인 스레드 ID : {0}",
                Task.CurrentId);

            for (long i = start; i < 100000000; i++)
            {
                sum += i;
            }
            return sum.ToString();
        }

        static void Main(string[] args)
        {
            Task<string> task = new Task<string>(Calc, 1L);

            task.Start();

            Console.WriteLine("추가 스레드의 ID {0}",
                task.Id);

            Console.WriteLine("추가 스레드에 제공된 상태 값 : {0}",
                task.AsyncState.ToString());

            while (!task.IsCompleted)
            {
                if (task.Status == TaskStatus.Running)
                {
                    Console.Write(".");
                }
            }

            Console.WriteLine("");
            //여기서 추가 스레드가 끝날 때 까지 기다린다.
            Console.WriteLine(task.Result);
            System.Diagnostics.Trace.Assert(
                task.IsCompleted);
        }
    }
}

<코드1> 유용한 속성을 사용한 예제

<코드1>은 기본적으로 유용하게 사용되는 속성들을 활용한 간단한 예제입니다. 우선 결과를 볼까요?

추가 스레드의 ID 1
현재 이 메서드를 실행중인 스레드 ID : 1
추가 스레드에 제공된 상태 값 : 1
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
................................................................................................................................................................
...............................................................................................................................................................
................................................................................................................................................................
-중략-
................................................................................................................................................................
......................................................................................
4999999950000000
계속하려면 아무 키나 누르십시오 . . .
<결과1> 실행 결과

그럼 속성을 하나씩 확인 해보면서 왜 저런 결과가 나왔는지 확인해보죠.

 속성명  설명 
 Task.CurrentId  현재 Task.CurrentId 호출을 처리하는 스레드의 Id
 AsyncState  스레드에 추척가능한 값을 부여한다. 예를 들어서 스레드가 3개가 있고, 각각 1,2,3 이라는 값을 부여했다면, 각각의 스레드의 AsyncState는 1,2,3 이므로 이 값을 다른 용도로 사용할 수도 있고, 스레드를 구별하는데 사용할 수도 있다. 
 Id  각각의 작업에 부여되는 고유한 Id
 Status  현재 작업의 상태를 표시한다. Created, WatingForActivation, WaitingForRun, Running, WaitingForChildrenToComplete, RanToComplete, Canceled, Faulted등이 있다. 
<표1> 유용한 속성들

네 각각의 속성을 한번 정리해봤습니다. 어헣. 추가 스레드의 아이디는 1이었죠. 그리고, Calc메서드를 실행하는 스레드도 역시 아이디가 1인 추가 스레드이기 때문에, Calc메서드 안에서 Task.CurrentId로 현재 실행중인 스레드의 아이디를 출력하면 1이 출력되는 것이구요. 스레드를 생성하면서, 상태값으로 1을 넘겨줍니다. 그래서, 그 상태값을 Calc메서드 내부에서 받아서 사용하기도 하고, 추후에 스레드의 상태값을 출력해볼 수도 있는 것이죠.


- 스레드 자세히 들여다 보기

그럼 비주얼 스튜디오에 새로 추가된 툴을 이용해서, 어떤 스레드가 생성되는지 한번 확인해보도록 하겠습니다. 아마, 뒤에서 더 자세하게 설명드리겠지만, 오늘은 그냥 간단하게 살펴보는 정도로 하도록 하죠. <코드1>에 아래와 같이 코드를 추가합니다.

Debugger.Break();

Console.WriteLine("추가 스레드의 ID {0}",
    task.Id);

Console.WriteLine("추가 스레드에 제공된 상태 값 : {0}",
    task.AsyncState.ToString());
           
while (!task.IsCompleted)
{
    if (task.Status == TaskStatus.Running)
    {
        Debugger.Break();
        Console.Write(".");
    }
}

<코드2> 수정한 코드

Debugger클래스는 System.Diagnostics에 정의되어 있는데요, 브레이크 포인트를 걸지 않아도, Break메서드가 호출된 곳에서 실행을 멈추고 디버거를 사용할 수 있습니다. 다만, 그냥 Debug모드로 놓고 컴파일하지 않고 실행(Ctrl + F5)을 하면, 계속 에러가 나니 주의하시구요.

우선 F5로 실행을 하신다음에, 첫번째 브레이크 포인트에 걸리면, '디버그'메뉴에서 '창'메뉴로 들어가서 '스레드', '병렬스택', '병렬작업'을 띄웁니다. 그리고 '스레드'를 보면,

<그림1> 스레드 창

다음과 같이 현재 떠있는 스레드의 목록을 볼 수 있습니다. 저 중에, '.NET SystemEvents'라고 된 스레드는 이벤트의 발생을 주시하고 있는 스레드이구요, 'vshost.RunParkingWindow'라고 된 스레드는 비주얼 스튜디오 호스팅 프로세스입니다. 그리고 '주 스레드'는 현재 Main메서드를 실행 중인 스레드를 말하구요. 그리고 ID가 4112라고 된 스레드가 추가로 생성한 스레드입니다. 어째서 그런지 확인을 해볼까요? 병렬 스택 창을 한번 확인해보죠.


<그림2> 병렬 스택 창

위 그림은 병렬 스택 창에서 스레드 그룹의 제목('4개 스레드'라고 쓰인 부분)에 마우스를 올리면 어떤 스레드가 있는지 보여주는 장면입니다. 총 3개의 스레드가 있는데, 앞에서 설명드린 3개의 스레드외에 작업자 스레드가 하나 있는 걸 보실 수 있습니다. 바로 저 스레드가 우리가 추가로 생성한 스레드인거죠. 그런데, 호출 스택에 Main메서드를 실행하는 주 스레드만 한 단계 진행한 걸 볼 수 있는데요, 그렇다면 아직 추가 스레드에 작업이 물린 상태는 아닌 것 같습니다. 그러면, F5키를 눌러서 다음으로 넘어가 볼까요? 그리고 병렬 스택을 보면,


<그림 3> 바뀐 병렬 스택 창

이미 추가 스레드도 작업에 들어간 상태이다 보니, 추가 스레드가 한 단계 더 진행해서, Calc메서드를 실행하고 있다고 나옵니다. 그리고 병렬 작업 창을 한번 볼까요?

<그림 4> 병렬 작업 창

상태는 실행 중(== TaskStatus.Running)이고, 할당된 스레드는 4112번 작업자 스레드라는 걸 확인할 수 있습니다. 이런 디버깅 툴을 잘 활용하면, 복잡한 병렬 프로그래밍을 하는 데 좀 더 편안하게 작업할 수 있겠죠? 이 디버깅 툴에 대해서는 나중에 좀 더 자세하게 설명드리도록 하지용.


- 마무리

앤더스 헬스버그는 C#에 영향을 미치는 3대 트렌드가 선언적, 동시적, 동적 프로그래밍 이라고 했는데요, 선언적 프로그래밍은 이미 LINQ를 통해서 편하게 지원되고, PLINQ로 확장이 되었죠. 그리고 동적 프로그래밍은 dynamic타입과 DLR을 통해서 지원이 되구요. 그리고 동시적 프로그래밍은 TPL과 이런 다양한 툴을 통해서 좀 더 제대로된 지원을 하고 있습니다. 제대로만 쓴다면, 비주얼 스튜디오 2010에서 쫌 편하게 작업할 수 있겠네요. 늘 문제가 되는건 제대로 쓸 줄도 모르면서 불평만 하는 저 같은 양민이져 어헣-_-.


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
2. http://msdn.microsoft.com/en-us/library/dd554943.aspx
3. http://msdn.microsoft.com/en-us/library/microsoft.win32.systemevents.aspx
4. http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/f8ccec3a-25db-4d3b-a90a-e758f6243356/

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
- 작업해본 적이나 있수?

물론이죠-_-;; 이 나이에 작업해 본 적도 없으면, 마법사 정도가 아니라, 신이 됐겠죠. 어헣. 오늘의 작업은 그 작업은 아니고... 스레드와 관련된 작업입니다. 부디 오해 없으시길 바라고, 작업의 기본은 다른 연예 서적에서 얻으시길.


- Task 시작하기.

지난 포스트에서 했던 예제를 한번 돌아보겠습니다.

using System;
using System.Threading.Tasks;

namespace Exam2
{
    class Program
    {
        static void Main(string[] args)
        {
            const int max = 10000;

            //현재 작업중인 스레드외에 추가로 스레드를 생성
            Task task = new Task(() =>
                {
                    for (int count = 0; count < max; count++)
                    {
                        Console.Write("|");
                    }
                });

            //추가 스레드 시작
            task.Start();

            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            task.Wait();
        }
    }
}

<코드1>

Main메서드를 실행하는 스레드 외에 또 하나의 스레드를 추가로 생성해서, 두 개의 스레드로 화면에 다른 문자열을 출력하는 예제였죠. 이 예제를 보면, Task라는 클래스를 사용하고 있습니다. 이 클래스는 닷넷 프레임워크 4.0에 새롭게 추가된 클래스인데요. 기존의 멀티스레드 프로그래밍을 한 단계 높은 추상화를 통해서 프로그래머의 실수를 줄이고, 좀 더 직관적인 코드를 작성할 수 있게 해주는 TPL(Task Parallel Library)에 포함되어서 추가된 클래스입니다. 중심에 있는 클래스라고 볼 수 있죠.

Task클래스는 관리되지 않는 스레드를 한단계 감싸서 추상화를 시킨 클래스입니다. 내부적으로 스레드 풀을 사용하는 데요, 내부적으로는 System.Threading.ThreadPool을 사용해서 요청에 따라 스레드를 새로 생성하거나, 이미 생성된 스레드를 재활용해서 부하를 줄입니다.

새로운 Task가 실행할 동작은 델리게이트를 통해서 명시해주는데요, <코드1>에서 굵게 처리된 부분이 바로 그 부분입니다. 카운트에 따라서 문자열을 출력하는 델리게이트를 생성자에 넘겨주고 있는 거죠. 물론, 이렇게 델리게이트를 넘겨준다고 해서 바로 스레드가 실행되는 건 아닙니다. Start메서드를 통해서 실행을 해줘야만 스레드가 실행되는 것이죠.


- Task가 끝날 때?

그러면, <코드1>은 두개의 스레드가 실행이 되면서 서로 다른 문자열을 번갈아 가면서 출력하겠죠. 여기서 한가지 생각해볼게 있습니다. 콘솔 어플리케이션은 Main메서드의 실행으로 시작하고, Main메서드의 끝과 함께 종료됩니다. 그렇다면, Main메서드의 실행을 맡은 스레드가 종료되었는데, 추가로 생성한 스레드의 작업이 안끝났다면 어떤 일이 벌어질까요?

//현재 작업중인 스레드외에 추가로 스레드를 생성
Task task = new Task(() =>
    {
        for (int count = 0; count < max; count++)
        {
            Console.Write("|");
        }
        Console.WriteLine("추가 스레드 끝");
    });

//추가 스레드 시작
task.Start();

//현재 작업중인 스레드에서도 반복문 시작
for (int count = 0; count < max; count++)
{
    Console.Write("-");               
}
Console.WriteLine("메인 스레드 끝");

//혹시 현재 스레드가 빨리 끝나더라도,
//추가 스레드가 끝날 때 까지 기다리기.           
//task.Wait();

<코드2>

<코드1>을 <코드2>와 같이 수정한 다음에 실행해보죠. 그러면, 둘 중의 어떤 스레드가 빨리 끝날까요? 그건 그때 그때 다릅니다-_- 그래서 아래와 같은 두 경우가 생길 수 있죠.

||||-----||||||||--------------|||||||||||||-------------||||||||||||-----------||||||---------------||||||||||||------------|||||||||||-----------||||||||||||--|||||||||||||-------|||||||||||-------------||||||||||||--------------||||||||||----------------|||---------------||||||||||||||-----||-------------||||||||||||----------------|||||||||||||-------------||||||||||||-------------|||---|------||||||||||||||-----------------|---------------||||||||||||||---------------메인 스레드 끝
|계속하려면 아무 키나 누르십시오 . . .
<결과1> 메인스레드가 먼저 끝나는 경우

|----------------|||||||||||||-----------||||||||||||||---------||||||||||||-----||||||||||||||||---------------||||||||||||||--------------||-----------||||||||------------|||||||||||||||-------------|--------------|||||||-------------|||||---||추가 스레드 끝----------------------------------------------------------------------------------------------------------------------------------------------------------메인스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과2> 추가 스레드가 먼저 끝나는 경우

<결과2>는 추가 스레드가 먼저 끝나면서 모든 결과가 출력이 되었지만, <결과1>은 Main메서드의 실행을 맡은 메인 스레드가 먼저 끝나면서 프로그램이 종료되었고, 따라서 추가스레드의 나머지 결과는 날아가 버렸습니다. Wait메서드는 메인 스레드가 먼저 끝나더라도, 추가 스레드가 끝날 때까지 기다리게 하는 역할을 합니다. 그래서 메인 스레드가 빨리 끝나더라도, 항상 추가 스레드의 결과까지 제대로 출력되게 되는 것이죠.

||||||||--------|||||||||||||||--------------|||||||||||||------------|||||||||||||--------||||
||-메인 스레드 끝||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||추가 스레드 끝
계속하려면 아무 키나 누르십시오 . . .
<결과3> Wait메서드를 사용한 경우


- 가는 길은 여러갈래.

앞에서 스레드의 시작은 Start메서드를 통한다고 말씀 드렸지만, 항상 그런 것은 아닙니다. 생성과 동시에 실행을 시킬 수도 있습니다.

Task task = Task.Factory.StartNew(() =>
                {
                    for (int count = 0; count < max; count++)
                    {
                        Console.Write("|");
                    }
                    Console.WriteLine("추가 스레드 끝");
                });
<코드3> 생성과 동시에 스레드 시작

<코드1>의 Task생성 부분을 <코드3>과 같이 수정하고, Start메서드 호출부분을 주석처리하면, 동일한 결과를 얻을 수 있습니다.

그리고 <코드1>에서는 메인스레드가 추가 스레드가 끝날 때까지 기다리기 위해서 Wait메서드를 사용했지만, 다른 방법도 있습니다. 만약에 추가 스레드에 입력된 델리게이트가 결과값을 반환하고, 메인 스레드에서 그 결과값을 사용해야 한다면, 메인 스레드는 추가 스레드의 작업이 끝나서 결과가 나올 때까지 기다립니다. Wait메서드 없이도 말이죠. 어찌보면 당연한 이야기죠 ㅋ

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

namespace Exam3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<string> task = Task.Factory.StartNew<string>(
                () =>
                {
                    long sum = 0;
                    for (long i = 0; i < 100000000; i++)
                    {
                        sum += i;
                    }
                    return sum.ToString();
                });

            foreach (char busySymbol in BusySymbols())
            {
                if (task.IsCompleted)
                {
                    Console.WriteLine('\b');
                    break;
                }
                Console.WriteLine(busySymbol);
            }

            Console.WriteLine();
            //여기서 추가 스레드가 끝날 때 까지 기다린다.
            Console.WriteLine(task.Result);
            System.Diagnostics.Trace.Assert(
                task.IsCompleted);
        }

        private static IEnumerable<char> BusySymbols()
        {
            string busySymbols = @"-\|/-\|/";
            int next = 0;
            while (true)
            {
                yield return busySymbols[next];
                next = (++next) % busySymbols.Length;
                yield return '\b';
            }
        }
    }
}

<코드 4> 결과 기다리기.

<코드4>는 추가 스레드의 결과를 계속 기다리다가, 결과가 나오는 순간, 출력하고 끝납니다.


- 멀티스레드 쉽고만?

이라고 생각하시면 곤란하구요-_-;; 열심히 공부 중인데, 역시 어렵습니다. 다만, Task클래스가 상당히 많은 부분을 간소화 시켜 주기 때문에, 한 층 더 편해진 느낌이랄까요? 오늘 여기까지!


- 참고자료

1. Essential C# 4.0, Mark Michaelis, Addison Wesley
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 개념!!

군 생활을 하다 보면, 개념 없는 사람들을 많이 보게 됩니다. 가끔은 '내가 평생 이런 놈을 다시 볼 수 있을까?'싶은 사람도 보게되죠. 그리고 인간관계란 과연 무엇인지 원점에서 다시 생각해 보게 됩니다. 개념 없는 사람들도 사회에서 담당하는 역할이 있는 셈이죠. 오늘은 병렬 프로그래밍에 필요한 개념들을 같이 채워볼까 합니다. 개념 충만한 사람들은 훠이훠이~ 절로 가시고(저는 종교의 다양성을 존중합니당. 교회로 가실 분은 교회로..), 개념 없는 사람들만 저랑 같이 개념을 채워 보시져. 훗.


- 기본 개념!!!

스레드(Thread)

스레드는 다른 순차적 명령 집합(sequence of instructions)과 동시적으로 실행 가능한 순차적 명령 집합을 이야기 합니다. 예를 들면, 뭔가 작업을 처리하는 스레드가 있고, 그 작업의 현황을 화면에 표시하는 스레드가 있는 것 처럼 말이죠. 두 스레드는 서로 다른 순차적 명령 집합을 가지며, 동시적으로 실행 가능하죠. 그리고 두 개 이상의 스레드를 동시적으로 실행하는 것을 멀티스레딩(multithreading)이라고 합니다. 흔히 이야기 하는 동시성이나 병렬성도 최대한 효율적으로 스레드를 여러개 사용하는 것이므로, 멀티스레드의 범주에 들어간다고 할 수 있겠습니다. 운영체제는 이런 멀티 스레드를 동시적으로 처리하는 것 처럼 보여주기 위해서 시간 쪼개기(time slicing)이라는 기법을 활용합니다. 즉, 사람이 눈치채지 못할 정도로 빠른 시간만큼 한번에 하나씩 스레드를 처리하는 것이죠. 단순하게 생각해서, 음악을 들으면서 비주얼 스튜디오로 코딩을 한다면, 사람이 눈치채지 못할 정도의 속도로, 예를 들면 1/24초 정도의 속도로 한번은 음악재생, 한번은 비주얼 스튜디오, 또 한번은 음악 재생, 또 한번은 비주얼 스튜디오 이런식으로 번갈아 가면서 처리하는 것이죠. 이걸 문맥 교환(context switching)이라고 합니다. <그림1>을 보시죠.

<그림1>본격 OS 스케쥴링 그림.jpg

<그림1>에서 나온 것 처럼, 사용자가 눈치채지 못 할만큼의 속도로 두 작업을 번갈아 가면서 CPU가 처리하도록 OS가 스케쥴을 조절하는 것이죠. 그런데 만약, 스레드가 너무 많아서 제대로 CPU를 점거하지도 못하고, 작업을 계속해서 교체하게 되면 어떻게 될까요? 실제로 CPU가 작업을 처리하는 시간보다, 문맥 교환에 더 많은 시간이 들어가게 되므로, 음악이 벅벅 끊기는 등의 지름유발상황이 생기겠죠. 그럼, 시간 쪼개기를 한번 시뮬레이트 해 볼까영? 어헣.

using System;
using System.Threading.Tasks;

namespace Exam2
{
    class Program
    {
        static void Main(string[] args)
        {
            const int max = 10000;

            //현재 작업중인 스레드외에 추가로 스레드를 생성
            Task task = new Task(() =>
                {
                    for (int count = 0; count < max; count++)
                    {
                        Console.Write("|");
                    }
                });

            //추가 스레드 시작
            task.Start();

            //현재 작업중인 스레드에서도 반복문 시작
            for (int count = 0; count < max; count++)
            {
                Console.Write("-");
            }

            //혹시 현재 스레드가 빨리 끝나더라도,
            //추가 스레드가 끝날 때 까지 기다리기.           
            task.Wait();
        }
    }
}

<코드1>아주 단순한 코드

<코드1>은 아주 단순한 코드입니다. 콘솔 어플리케이션의 실행을 담당하는 스레드 외에, 추가로 스레드를 하나 더 생성해서, 두 스레드에서 똑같은 반복문을 돌면서 서로 다른 문자를 출력하게 하는 거죠. 결과를 볼까요?

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

<결과1>평범한 결과

서로 다른 문자가 출력되는 곳이 바로, 시간 쪼개기로 인한 문맥 교환이 일어나는 곳입니다. 정해진 시간만큼 한 스레드가 CPU를 점거하고 작업을 하는 것이죠. 그러면, 약간 방해를 해볼까요? 코드를 다음과 같이 약간 수정해봅니다.

Console.ReadLine();

//추가 스레드 시작
task.Start();

<코드2>수정된 아주 평범한 코드

그리고 이 프로그램이 사용자의 입력을 기다리는 동안, 다음과 같이 프로세스의 우선순위를 낮춰버립니다.

<그림2>프로세스 우선순위 낮추기 ㅋ

그리고 한번 실행해 볼까요? 프로세스의 우선순위가 낮아지면, 실행중인 작업이 우선순위가 높은 다른 프로세스에게 방해를 받을 가능성이 높아지는 거죠.

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

<결과2>낮은 우선순위 결과

그런데, <결과2>를 보니, 얼마나 방해를 받은 건지 솔직히 잘 모르겠네요.('-'가 마지막에 막 출력된 건, '|'를 출력하는 스레드가 빨리 작업을 끝내서 입니당) 제 컴이 쿼드에 2.7GHz짜리인데, 아마도 이정도로는 방해가 안되는 거 같습니다. 그래서! 비주얼 스튜디오 2010 20개랑, 음악을 틀고, 바이러스 검사를 돌리고, 인터넷 익스플로러 8도 창을 7개정도 띄워놓고 실험을 해봤습니다.

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

<결과3>갖가지 태클을 가한 결과

<결과3>을 보시면, 어느 정도 패턴이 불안정 해진 걸 보실 수 있습니다. 제대로 처리도 못하고, '|'이거 딱 한개 출력하고 문맥 교환이 일어난 부분도 꽤 보이네요.


원자성(Atomicity)

원자성은 뭘까요? 원자성을 만족하는 조건을 보지요. 아래 조건 중에서 하나를 만족하면 원자성을 만족하는 것입니다.

1. 작업을 처리하기 위한 명령어 집합은 다른 작업이 끼어들기 전에 반드시 완료되어야 한다.
2. 작업을 처리 중인 시스템의 상태는, 작업을 처리하기 전의 시스템의 상태로 돌아갈 수 있어야 한다. 즉, 아무 작업도 처리되지 않은 시스템의 상태로 돌아갈 수 있어야 한다.

예를 들면, 은행 계좌에서 돈을 송금하는 걸 생각해보면요. 돈을 송금하기위한 몇가지 단계가 있습니다. 일단 계좌에 돈이 있는지 확인하고, 보낼 계좌가 존재하는지 확인하고, 그리고 돈을 실제로 송금하는 것이죠. 이렇게 송금을 위한 각 단계를 밟는 와중에, 돈을 인출한다던가 하는 작업이 끼어들면 안되겠죠. 그리고 송금 절차가 완료되기 전에는 계좌에 아무런 변화가 없어야 하는 거죠.

C#에서 num++같은 연산을 보면, num의 값을 가져오고, num의 값을 1 증가시키고, num에 증가된 값을 넣는 절차를 밟는데요. ++연산자는 원자성을 만족하지 못합니다. 그래서 num에 증가된 새 값을 넣기 전에, 다른 스레드가 원래 값을 변경 시켜버린다던가 하는 일이 벌어질 수 있죠.


데드락(Deadlock)

데드락은 죽음을 잠근다는 뜻이므로, 죽지 않는다는 뜻입니다. 네, 저를 죽이려고 달려드는 사람들의 모습이 보이네요. 모두들 잠깐만 진정하시고....-_- 이건 서로 볼을 꼬집고서, '니가 먼저 안놓으면, 나도 안놓을 꺼임'하고 외치고 있는 꼬마들을 생각하시면 됩니다. 꼬맹이들은 괜한데서 자존심을 세우죠. 그래서 먼저 놓으면 지는거라고 생각해서 안 놓습니다. 그래서 서로 계속해서 볼을 꼬집고 질질 짜게 되는 거죠. ㅋㅋㅋ

한 스레드는 A라는 자원을 점거하고, B를 가지고 와야 A를 놓아줍니다. 그리고 또 다른 스레드는 B라는 자원을 점거하고, A를 가져와야 B를 놓아준다고 해보죠. 그러면 이 스레드들은 서로 상대방이 가진 자원을 하염없이 기다리는 상황이 됩니다. 죽을 때까지 그 상태로 기다리겠죠. 그래서 이건 데드락입니다. 어헣.


- 정리하면서

위에서 설명드린 원자성이나 데드락은 여러개의 스레드에서 어떤 순서로 명령어를 실행하느냐에 따라서 복잡하게 발생합니당. 이런 프로그램을 짜려면 머리 빠지겠죠. 아이 무셔워라. 그래서 조심해야 하는 것이고, 이런 실수를 최대한 줄여주는 도구가 있어야 하는 것이죠!!

이게 왠 운영체제 수업이냐 하고 반감을 가지시는 분덜도 있으시리라 생각이 됩니다. 모.. 복습했다 생각하시고 어헣. 좋은게 좋은거니깐.. 어헣.


- 참고자료

1. http://www.cafeaulait.org/course/week11/03.html
2. Essential C# 4.0, Mark Michaelis, Addison Wesley

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

WCF 서비스의 동시성(Concurrency) - 2

WCF 2010.03.30 09:00 Posted by 오태겸(RuAA)
지난 포스팅에 이어서 WCF 서비스의 동시성에 대해 이야기 해보겠습니다.
다른 설명 하지 않고, 지난 포스팅에서 했던 것 처럼 예제를 우선 보고 얘기를 진행해볼까 합니다.

Implementing a Singleton

단 하나의 서비스 인스턴스만이 생성되며, 인스턴스에서 동작하는 스레드 역시, 단 하나만 생성되게 하는 경우에 대해 먼저 살펴보겠습니다.
저번 포스팅에서 사용했던 서비스 클래스의 코드를 다음과 같이 굵은 글씨체 부분만을 수정하여 서비스를 실행해 보시기 바랍니다.

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,

            ConcurrencyMode = ConcurrencyMode.Single)]

class ProductService : IProductService

{

    ProductService()

    {

        Console.WriteLine("{0}: !!", DateTime.Now);

    }
          ... 생략 ...
}

이 코드를 실행하면 다음과 같은 결과를 확인할 수 있을 것입니다.

[서버]


[클라이언트]


결과가 여러분이 예상했던 것과 일치하나요?? ㅎ

이 경우 역시 서비스의 인스턴스는 서버 측 결과 화면을 통해 클라이언트의 요청의 수와는 관계없이 하나 만이 생성됨을 알 수 있습니다.
그리고, 클라이언트에서 서비스를 호출하는 방식으로 비동기 방식을 사용했던 것 기억하실겁니다. 결과 그림만을 봐서는 잘 모를 수도 있지만, 이 때문에 클라이언트 측 결과 화면을 보면, 지연시간 없이(각 호출에 대한 결과를 받지 않아도) 서비스를 연속적으로 세 번 호출함을 볼 수 있습니다.
하지만, 이 호출에 대한 결과는 각각 5초간의 지연시간을 두고 화면에 출력하는데, 이는 서버측 결과 화면을 보면 그 이유를 알 수 있습니다. 만약, 서비스 인스턴스가 여러 개의 스레드를 만들어 요청을 처리했다면, 클라이언트의 각 요청에 대한 결과를 거의 동시에 받을 수 있겠지만, 이 경우에는 하나의 스레드 만이 동작하기 때문에 각 요청을 한번에 하나씩 처리할 수 있어 이러한 결과를 얻을 수 있는 것이죠. 참고로, 각 요청에 대한 처리는 FIFO(First In First Out)의 순서로 동작합니다.

서비스 인스턴스에서 단 하나의 스레드 만이 동작한다는 것은 서버 측 결과에서 Thread ID 값이 3으로 동일한 것을 봐도 증명이 가능합니다. 가끔, 이 thread id의 값이 각 요청마다 다른 값이 나올 수도 있습니다. 그렇다고 잘못된 결과값은 아닙니다. ConcurrencyMode.Single한번에 하나의 스레드만이 동작한다는 것을 명시하는 거지, 단 하나의 스레드만이 만들어진다라는 의미는 아니거든요~,, 약간 헷갈릴 수도 있는 부분인 것 같으니, 꼭 명심해주세요,, ㅎ

이 경우처럼 싱글 인스턴스, 싱글 스레드는 한번에 하나의 클라이언트 요청만을 처리할 수 있기 때문에 throughput을 감소시킨다는 단점이 있지만, 반면에 시스템 자원(resource)에 동시 접근 같은 문제가 일어나지 않아서, 이에 대한 추가적인 관리가 필요하지 않다는 장점도 있습니다.

Session-Level Instances

이번에는 세션 모드가 적용되었을 때, 서비스의 인스턴스가 어떻게 생성되는지 한번 살펴보도록 하겠습니다.

우선, 서비스에서 세션을 지원하도록 하기 위해 서비스 계약의 특성값을 다음과 같이 수정합니다.

[ServiceContract(Namespace = "http://RuAAService.co.kr/",

                     SessionMode = SessionMode.Required)]

interface IProductService

{

    [OperationContract]

    Product GetProduct();

}


SessionMode에 Required 값을 적용하여 이 서비스가 세션 모드를 지원해준다는 것을 명시해 줍니다.
다음은 ServiceBehavior 특성에서 InstanceContextMode의 값을 PerSession 으로, ConcurrencyMode의 값을 Multiple 로 수정을 해줍니다. 그리고 GetProduct 메서드가 한 인스턴스에서 몇 번 호출이 이루어지는지를 체크하기 위해 n_Calls 라는 이름의 필드를 추가해 주었습니다. lockThis 필드는 n_Calls 필드의 값을 증가시킬 때 다른 스레드에서 동시에 n_Calls의 값을 바꾸지 못하도록 하기 위한 목적으로 선언해주었습니다.


[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,

            ConcurrencyMode = ConcurrencyMode.Multiple)]

class ProductService : IProductService

{

    object lockThis = new object();

    private int n_Calls = 0;

       
       ... 중간 생략 ...

   
    public
Product GetProduct()

    {

        Console.WriteLine("{0} : GetProduct , Thread Id {1}", 
                            DateTime
.Now, Thread.CurrentThread.ManagedThreadId);

        Thread.Sleep(1000);

 

        Product p = new Product();

        p.ProductId = 1234;

        p.ProductName = "ABC Chocolate";

        p.Price = 1500.0;

        p.Company = "Lotteee";

        p.CreateDate = DateTime.Parse("2010-01-22");

 

        lock (lockThis)

        {

            p.calls = ++n_Calls;

        }

 

        return p;

    }

}


코드를 다 수정하셨으면, 결과를 확인해보도록 하겠습니다.

당연히, 서버 측 콘솔 어플리케이션을 실행 시키신 후에, 클라이언트 어플리케이션을 실행시켜야겠죠,,^^
아,, 이런~ 저와 같은 방법으로 수정을 한 후에 실행 시키면, 아마 예외가 발생하실겁니다.

예외의 이유는 간단합니다. Service Contract에서 세션 모드의 값을 Required로 설정을 해놓았는데, 실제 서비스를 호스팅할 때 세션을 지원하지 않는 BasicHttpBinding을 사용했기 때문입니다. 따라서, BasicHttpBinding을 WSHttpBinding 으로 수정해주시면 이 예외는 발생하지 않을 것입니다.

자~ 이 부분 수정을 다 하셨다면, 다시 실행을 해보도록 하겠습니다.

세션모드의 지원을 확인하기 위해서 클라이언트 어플리케이션을 두 개 연속해서 실행을 시킵니다.

다음은 서버 어플리케이션의 실행 화면입니다.


인스턴스가 두 개 생성된 것을 확인할 수 있네요,, 왜 인스턴스가 두 개 생성되었을까요?? 당연히 클라이언트 어플리케이션이 두 개 실행이 되었기 때문입니다. 세션을 지원하는 서비스이니깐요,,

자~ 다음은 두 클라이언트 어플리케이션의 실행화면입니다.





각 클라이언트는 호출한 횟수가 1~3인 것을 확인할 수 있습니다. 이것은 각 클라이언트 마다의 서비스 인스턴스가 따로 데이터를 유지한다는 것을 의미하는 것이죠. 이해 되셨죠?? ^^

이번 포스팅은 이것으로 마무리 하려 합니다.
이번에도 포스팅이 조금 늦었습니다. 바로 하려고 했는데 이게 마음처럼 쉽지가 않네요. ^^;;

어찌됐든, 다음 포스팅때 뵙도록 하겠습니다. ㅎ
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

WCF 서비스의 동시성(Concurrency) - 1

WCF 2010.03.10 09:00 Posted by 오태겸(RuAA)

봄이 오고 있네요,, 
날씨가 많이 따뜻해졌고, 해도 부쩍 길어졌음을 느낍니다.
최대한 빠른 시일 내에 포스팅을 하려 했는데, 그동안 무기력증(?)에 빠져있다보니,,
하는거 없이 시간만 보내버렸네요,, ^^;;
봄이 찾아온 만큼 새로운 마음가짐으로 다시 시작해보겠습니다. 아자~!

이번 포스팅의 주제는 WCF의 Behaviors 중에서 서비스의 동시성(Concurrency)을 컨트롤 할 수 있는 Behavior 입니다.

Behavior는 서비스가 동작할 때(그러니깐 런타임 시) 동작에 영향을 끼치는 클래스들로, 서비스 클래스의 특성으로 지정하거나, 환경 설정파일을 통해 지정할 수 있습니다.

Behavior와 관련된 여러 가지 내용 중 이번 포스팅에선 동시성(Concurrency)에 대해서 얘기해보도록 하겠습니다.

동시성이라 함은, 여러 task 들이 동시에 동작하는 것을 말합니다.
동시성은 다들 아시겠지만, 그리고 아주 당연하게도 시스템의 throughput(출력률)에 큰 영향을 끼칩니다. 일정 시간동안 처리할 수 있는 작업의 양이 커지기 때문이죠.

WCF 에서는 동시성을 컨트롤할 수 있는 두 종류의 behavior 가 있습니다. 바로 “InstanceContextMode”“ConcurrencyMode” 입니다.

InstanceContextMode는 생성되는 서비스의 인스턴스를 조절할 수 있는 behavior로 다음과 같은 세 종류의 값으로 설정할 수 있습니다.

  • Single : 이 값은 서비스로 들어오는 모든 요청을 하나의 인스턴스에서 처리하도록 설정합니다.
  • PerCall : 서비스로 들어오는 요청마다 서비스의 인스턴스가 만들어지도록 하기 위한 설정입니다.
  • PerSession : 클라이언트 세션마다의 서비스 인스턴스를 생성하기 위한 설정이며, 만약 세션을 사용하지 않는 채널일 때, 이 값으로 설정이 된다면, PerCall과 같은 방식으로 동작합니다.

그리고, InstanceContextMode의 기본값은 PerSession으로 따로 어떠한 값도 설정되어 있지 않은 경우엔 세션 수에 따라 서비스 인스턴스가 생성됩니다.

WCF에서 동시성을 조절할 수 있는 또 다른 모드인 ConcurrencyMode는 하나의 서비스 인스턴스 내에서 동작하는 스레드를 통한 동시성을 컨트롤하는 behavior입니다.
다음은 ConcurrencyMode에서 설정할 수 있는 값에 대한 설명입니다.

  • Single : 하나의 서비스 인스턴스 내에 오로지 하나의 스레드만이 동작하도록 설정하는 값입니다. 따라서, 이 값으로 설정되어 있는 경우엔 스레딩 문제를 고려하지 않아도 된다는 장점이 있습니다.
  • Reentrant : 이 설정 역시 하나의 서비스 인스턴스에서 하나의 스레드만이 동작하도록 하는 설정값입니다. 하지만, 이 설정값이 Single과 다른 점은 하나의 스레드가 동작하는 도중에 다른 작업이 처리될 수 있다는 것입니다. 이 작업의 처리가 완료되면 이 전의 작업이 계속해서 동작됩니다.
  • Multiple : 하나의 서비스 인스턴스에서 하나 이상의 스레드가 동작할 수 있도록 하는 설정입니다. 이 값으로 설정되어 있는 경우엔 여러 개의 스레드에서 서비스 개체를 변경할 수 있기 때문에 항상 동기화와 상태 일관성을 처리해 주어야 합니다.

ConcurrencyMode와 InstanceContextMode의 값을 적절하게 조합하면, 서비스의 기능에 맞게 동시성과 인스턴스 관리를 할 수 있습니다. 지금 이러한 내용을 글로 써내려가봤자 설명하기도 힘들고, 받아들이기도 힘이 들겁니다. 따라서 이러한 내용은 역시 실제 코드를 작성하고, 결과를 보면서 이해하는게 가장 쉽고 빠른 방법이겠죠 ^^

네~ 이제 InstanceContextMode와 ConcurrencyMode의 값을 적절하게 조합하여 서비스에 적용하는 실습을 해보도록 하겠습니다.

우선, 가장 먼저 세션을 사용하지 않는 환경에서 InstaceContextMode와 ConcurrencyMode의 기본값을 사용한 서비스를 구현해보겠습니다. InstanceContextMode의 기본값은 PerSession 이며, ConcurrencyMode의 기본값은 Single 입니다. 이 기본값은 따로 설정해주지 않아도 적용된다는거 아시죠? ㅎ

다음은 서비스를 구현한 클래스의 코드 입니다.

class ProductService : IProductService

{

    ProductService()

    {

        Console.WriteLine("{0}: 서비스의 새로운 인스턴스 생성!!", DateTime.Now);

    }

 

    public Product GetProduct()

    {

        Console.WriteLine("{0} : GetProduct 호출, Thread Id {1}", DateTime.Now,
Thread.CurrentThread.ManagedThreadId);

        Thread.Sleep(5000);

 

        Product p = new Product();

        p.ProductId = 1234;

        p.ProductName = "ABC Chocolate";

        p.Price = 1500.0;

        p.Company = "Lotteee";

        p.CreateDate = DateTime.Parse("2010-01-22");

 

        return p;

    }

}


저번 포스팅에서 사용했던 서비스의 코드를 살짝 수정 해보았습니다.
서비스 클래스 생성자를 만들어 단순하게 인스턴스가 생성되었다는 메시지를 출력해주는 코드를 추가하였구요, GetProduct 메서드 내에서는 현재 스레드의 ID 값을 출력해주는 코드를 추가하였습니다.

다음은 이 서비스를 호출하는 클라이언트 코드입니다. 코드를 보시면 아시겠지만 클라이언트에서 서비스 메서드를 비동기로 호출하고 있습니다. 혹시 WCF 서비스를 비동기로 호출하는 클라이언트를 만들어보시지 않은 분이 계시면 제가 예전에 올렸던 포스팅을 참고해주시기 바랍니다. (http://ruaa.tistory.com/entry/async-call) 자세한 설명은 없지만 대충은 이해하실 수 있으실겁니다 ^^;;

namespace MySvcAsyncClient

{

    class Program

    {

        static int c = 0;

        static void Main(string[] args)

        {

            ProductServiceClient proxy = new ProductServiceClient();

            for (int i = 0; i < 3; i++)

            {

                Console.WriteLine("{0}: GetProduct 메서드 호출", DateTime.Now);

                proxy.BeginGetProduct(GetProductInfoCallback, proxy);

                Thread.Sleep(100);

                Interlocked.Increment(ref c);

            }

            while (c > 0)

            {

                Thread.Sleep(100);

            }

        }

 

        static void GetProductInfoCallback(IAsyncResult ar)

        {

            ProductInfo productInfo = ((ProductServiceClient)ar.AsyncState)
                                           .EndGetProduct(ar);

            Console.WriteLine("{0} : ProductName : {1}",
                                        
DateTime.Now, productInfo.Name);

            Interlocked.Decrement(ref c);

        }

    }

}


Main 메소드 내에서는 for 문을 사용하여 3번 반복하여 GetProduct 메소드를 비동기로 호출하고 있으며, 각각의 비동기 호출에 의한 작업이 끝이 나면 AsyncCallback 대리자인 GetProductInfoCallback 메소드가 호출되며, 서비스에서 받은 Product 데이터를 화면에 출력해줍니다.

이렇게 코드를 작성하고 나면, 역시 결과가 궁금해 질겁니다. 다음은 이 코드에 대한 결과 화면입니다.

[서버]


[클라이언트]


클라이언트 측 결과 화면을 보면 동시에 서비스의 메소드를 세번 호출하는 것을 확인할 수 있습니다. 그리고 6초 정도의 시간 후에 차례대로 결과값을 가져와서 출력하는 것을 볼 수 있습니다.

서비스 측 결과 화면을 확인해 보면, 각 호출마다 생성자를 통해 새로운 인스턴스를 생성하고, 인스턴스 내에 하나의 스레드를 통해 GetProduct 메소드를 호출하는 것을 확인할 수 있습니다.

여기서 잠깐 의문이 들지도 모르겠습니다. 제가 분명, InstanceContextMode의 기본값은 PerSession 이라고 했는데 왜 서버에선 클라이언트의 호출마다 새로운 인스턴스를 생성한 것일까요?

답은 아주 간단합니다. 서비스를 호스팅할 때 사용했던 binding의 종류가 BasicHttpBinding 이었던 것 기억하시나요? BasicHttpBinding의 경우엔 세션을 사용하지 않기 때문에, 이 경우엔 실제로 InstanceContextMode.PerCall 과 같은 형식으로 동작하게 되는 것입니다.

기본값으로 설정한 경우를 알아봤으니, 이번엔 두 모드의 값을 바꿔서 서비스에 적용해보겠습니다.

인스턴스는 모든 호출에 대해 하나만 생성하도록하고, 스레드의 갯수는 하나 이상으로 만들 수 있게끔 설정한 후에 결과값을 살펴보죠~

앞에서 한번 언급했지만 서비스의 Behavior를 적용하는 방법은 서비스 클래스에 특성으로 설정하는 방법과 config 파일에 설정하는 방법이 있습니다. 여기서는 클래스에 특성으로 설정하는 방법을 사용해보겠습니다.

서비스 클래스의 코드를 다음과 같이 굵은 글씨로 적용된 부분만을 추가해보죠~

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,

            ConcurrencyMode=ConcurrencyMode.Multiple)]
class ProductService : IProductService

{

    ProductService()

    {

        Console.WriteLine("{0}: 서비스의 새로운 인스턴스 생성!!", DateTime.Now);

    }

 

    ... 생략 ...

}


이렇게만 수정한 후에 솔루션을 실행시켜 보면, 클라이언트 측 화면은 변화가 없지만 서버 측 결과 화면은 다음과 같이 변화된 것을 확인하실 수 있으실겁니다.



달라진 점이 무엇인지 보이시죠? ^^

네,, 맞습니다. 인스턴스가 하나만 생성되었다는 점이죠. 아~ 그러고보니 동작한 스레드의 ID 값들이 모두 다른 것도 보이네요. 이 말은 곧, 하나의 인스턴스에 여러 개의 스레드가 생성되었다는 것을 의미하는 것이겠죠. 앞에서 설정했던 InstanceContextMode의 값과 ConcurrencyMode의 값이 어떻게 서비스의 동시성에 적용되었는지 이해가 가실겁니다.

이 외에도 서비스의 동시성에 적용할 수 있는 두 모드의 조합이 더 있지만, 다음 포스팅에서 더 다루도록 하겠습니다. 글도 길어졌고, 아직 담아야 할 내용도 많으니깐요.
이번에는 정말 다음 포스팅때 까지 많이 걸리지 않을 것입니다. 약속드릴께요~ ^^;;

제 포스팅에 항상 댓글 남겨주시고 응원해주시는 분들께 감사 드리며, 또 너무 오랜만에 글을 남겨 죄송한 마음도 듭니다. 제가 잠깐 주춤하긴 했지만, 앞으로는 계속 꾸준한 모습 보여드리려 노력하겠습니다. ^^

감사합니다.
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
이번 글은 길이가 좀 깁니다. 내용은 복잡한 것이 아니니 길다고 중간에 포기하지 마시고 쭉 읽어주세요^^


이전 회에서는 PPL에 대한 개념을 간단하게 설명했고, 이번에는 PPL의 세가지 feature 중 태스크(Task)에 대해서 설명하려고 합니다. 태스크에 대한 설명은 이미 이전에 정재원님께서 블로그를 통해서 설명한 적이 있습니다. 정재원님의 글은 태스크 사용 예제 코드를 중심으로 설명한 것으로 저는 그 글에서 빠진 부분과 기초적인 부분을 좀 더 설명하려고 합니다.

 

태스크라는 것은 작업 단위라고 생각하면 좋을 것 같습니다. 작업이라는 것은 여러 가지가 될 수 있습니다. 피보나치 수 계산, 배열에 있는 숫자 더하기, 그림 파일 크기 변경 등 작고 큰 작업이 있습니다. 보통 크기가 큰 작업은 이것을 작은 작업 단위로 나누어 병렬 처리를 하기도 합니다.

 

PPL의 태스크는 작업을 그룹 단위로 묶어서 병렬로 처리하고 대기 및 취소를 할 수 있습니다.

 

 


태스크 핸들

태스크 핸들은 각각의 태스크 항목을 가리키며 PPL에서는 task_handle 클래스를 사용합니다. 이 클래스는 람다 함수 또는 함수 오브젝트 등을 태스크를 실행하는 코드로 캡슐화 합니다. 태스크 핸들은 캡슐화 된 태스크 함수의 유효 기간을 관리하기 때문에 중요합니다. 예를들면 태스크 그룹에 태스크 핸들을 넘길 때는 태스크 그룹이 완료 될때까지 유효해야합니다.


보통 태스크 관련 예제 코드를 보면 task_handle 대신 C++0x의 auto를 사용하는 편이 코드가 더 간결해지므로 task_handle 보다는 auto를 사용하고 있습니다.


 

 

unstructured structured Task Groups

태스크 그룹은 unstructured structured 두 개로 나누어집니다.

두개의 태스크 그룹의 차이는 스레드 세이프하냐 안하느냐의 차이입니다.

unstructured는 스레드 세이프 하고 structured는 스레드 세이프 하지 않습니다.


태스크 관련 예제에 자주 나오는 task_group 클래스는 unstructured 태스크 그룹이고, structured_task_group 클래스는 structured 태스크 그룹을 뜻합니다.

 

unstructured 태스크 그룹은 structured 태스크 그룹보다 유연합니다. 스레드 세이프 하며 작업 중 taks_group::wait를 호출하여 대기한 후 태스크를 추가한 후 실행할 수 있습니다. 그렇지만 성능면에서 structured 태스크 그룹이 스레드 세이프 하지 않으므로 unstructured 태스크 그룹보다 훨씬 더 좋으므로 적절하게 선택해서 사용해야 합니다.

 

structured 작업 그룹은 스레드 세이프 하지 않기 때문에 Concurrency Runtime에서는 몇가지 제한이 있습니다.

- structured 작업 그룹 안에 다른 structured 작업 그룹이 있을 경우 내부의 작업 그룹은 외부의 작업 그룹보다 먼저 완료해야 한다.

- structured_task_group::wait 멤버를 호출한 후에는 다른 작업을 추가한 후 실행할 수 없다.


 

 

초간단!!! 6단계로 끝내는 태스크 사용 방법


1. ppl.h 파일을 포함합니다.

   #include <ppl.h>

 

2. Concurrency Runtime의 네임 스페이를 선언합니다.

   using namespace Concurrency;

 

3. 태스크 그룹을 정의합니다.

  structured_task_group structured_tasks;

 

4. 태스크를 정의합니다.

  auto structured_task1 = make_task([&] { Plus(arraynum1, true); } );

 

5. 태스크를 태스크 그룹에 추가한 후 실행합니다.

  structured_tasks.run( structured_task1 );

 

6. 태스크 그룹에 있는 태스크가 완료될 때까지 기다립니다.

  structured_tasks.wait();

 

위의 순서대로 하면 태스크를 사용할 수 있습니다. 태스크 사용 참 쉽죠잉~ ^^.

참고로 여러 개의 태스크를 그룹에 추가하고 싶다면 6번 이전에 4번과 5번을 추가할 개수만큼 반복하면 됩니다.


* 4번의 Plus(arraynum1, true);는 하나의 태스크에서 실행할 함수입니다.

 


PPL의 태스크를 사용하면 병렬 프로그래밍을 간단한 6단계만으로 끝낼 수 있습니다. 만약 현재의 Win32 API로 이것을 구현하기 위해서는 학습에 많은 시간을 보낸 후 저수준의 API를 사용하여 구현해야 되기 때문에 구현 시간과 안정성에서 PPL의 태스크보다 손해를 봅니다.




태스크 그룹과 스레드 세이프

unstructured structured 태스크 그룹의 차이가 스레드 세이프 유무의 차이라고 했는데 이 말은

unstructured 태스크 그룹은 복수의 스레드에서 호출 및 대기를 할 수 있지만 structured 태스크 그룹은 그것을 생성한 스레드에서만 호출 및 대기를 할 수 있습니다.


예를 들면 스레드 A, 스레드 B가 있는 경우 스레드 A와 B에서 태스크를 실행 후 대기를 한다면 unstructured 태스크 그룹을 사용해야하고, 오직 하나의 스레드에서만(스레드 A에서만) 태스크를 실행 후 대기를 한다면 structured 태스크 그룹을 사용합니다.


스레드 세이프는 스레드 세이프 하지 않는 것보다 오버헤드가 발생합니다. 즉 스레드 세이프 버전은 스레드 세이프 하지 않은 버전보다 성능이 떨어진다는 것이죠.

그러니 태스크 그룹을 어떤 방식으로 사용할지 파악 후 스레드 세이프 필요성에 따라서 unstructured 태스크 그룹과 structured 태스크 그룹 중 상황에 알맞은 것을 선택해서 사용해야 합니다.




ps : 제가 8월 14일 글을 공개할 때 태스크 그룹의 스레드 세이프 특성을 잘 못 이해하여 잘못된 내용을 전달하였습니다. 그래서 오늘 글을 다시 수정하였습니다. ;;;;;;

다음부터는 틀린 글을 올리지 않도록 조심하겠습니다. ^^;;;;;;

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
병행 런타임 관련 두번째 예제로 event를 이용합니다. 여기서 이벤트는 뮤텍스 등과 같은 동기화 개체의 하나로 기존 Win32에서의 수동리셋이벤트(manual-reset event)와 같은 것을 말합니다.

기존 이벤트와 병행 런타임에서 제공하는 이벤트에는 한가지 중요한 차이점이 있습니다. 새로운 이벤트는 병행 런타임을 인식하여 작업이 블록되는 경우 다른 작업에 스레드를 양보(yield)합니다. 무조건 선점형으로 동작하는 기존 Win32 이벤트보다 더 지능적이고 효율적으로 동작하는 것이죠.

자, 그럼 코드를 살펴봅시다. 이 예제에서는 스케줄러의 병렬성을 2로 제한하고 그보다 많은 수의 작업을 병행 수행할 때, 기존 Win32 이벤트와 병행 런타임 이벤트를 각각 활용하는 경우 어떤 차이가 있는지 보여줍니다.

    1 // event.cpp : Defines the entry point for the console application.

    2 //

    3 // compile with: /EHsc

    4 #include <windows.h>

    5 #include <concrt.h>

    6 #include <concrtrm.h>

    7 #include <ppl.h>

    8 

    9 using namespace Concurrency;

   10 using namespace std;

   11 

   12 class WindowsEvent

   13 {

   14     HANDLE m_event;

   15 public:

   16     WindowsEvent()

   17         :m_event(CreateEvent(NULL,TRUE,FALSE,TEXT("WindowsEvent")))

   18     {

   19     }

   20 

   21     ~WindowsEvent()

   22     {

   23         CloseHandle(m_event);

   24     }

   25 

   26     void set()

   27     {

   28         SetEvent(m_event);

   29     }

   30 

   31     void wait(int count = INFINITE)

   32     {

   33         WaitForSingleObject(m_event,count);

   34     }

   35 };

   36 

   37 template<class EventClass>

   38 void DemoEvent()

   39 {

   40     EventClass e;

   41     volatile long taskCtr = 0;

   42 

   43     //태스크그룹을 생성하고 여러 태스크 사본을 스케줄링합니다.

   44     task_group tg;

   45     for(int i = 1;i <= 8; ++i)

   46         tg.run([&e,&taskCtr]{           

   47 

   48       //작업 부하를 시뮬레이션합니다.

   49             Sleep(100);

   50 

   51             //태스크 카운터를 증가시킵니다.

   52             long taskId = InterlockedIncrement(&taskCtr);

   53             printf_s("\tTask %d waiting for the event\n", taskId);

   54 

   55             e.wait();

   56 

   57             printf_s("\tTask %d has received the event\n", taskId);

   58 

   59     });

   60 

   61     //이벤트를 셋하기 전에 충분히 시간을 보냅니다.

   62     Sleep(1500);

   63 

   64     printf_s("\n\tSetting the event\n");

   65 

   66     //이벤트를 셋

   67     e.set();

   68 

   69     //작업들의 완료를 대기

   70     tg.wait();

   71 }

   72 

   73 int main ()

   74 {

   75     //스레드 둘만을 활용하는 스케줄러를 생성합니다.

   76     CurrentScheduler::Create(SchedulerPolicy(2, MinConcurrency, 2, MaxConcurrency, 2));

   77 

   78     //협력적 이벤트를 사용할 경우, 모든 작업들이 시작됩니다.

   79     printf_s("Cooperative Event\n");

   80     DemoEvent<event>();

   81 

   82     //기존 이벤트를 사용하면, Win7 x64 환경이 아닌한

   83     //ConcRT가 블록 상황을 인식하지 못하여 첫 두 작업만이 시작됩니다.

   84     printf_s("Windows Event\n");

   85     DemoEvent<WindowsEvent>();

   86 

   87     return 0;

   88 }


WindowsEvent 클래스는 기존 Win32 이벤트를 위한 랩퍼(wrapper) 클래스입니다. DemoEvent 함수 템플릿이 사용할 이벤트 형을 템플릿 인자로 받아 실제 작업을 하는 놈입니다. PPL(Parallel Patterns Library)의 task를 이용해 8개 작업을 만들고 각각에서 이벤트를 기다리도록 하고 있습니다.

결과는 다음과 같이 나올 겁니다.


병렬성이 둘로 제한되는 상황에서도 병행 런타임의 이벤트를 사용할 경우 각 작업이 블록될 경우 다른 작업에 스레드를 양보하기 때문에 8개의 작업이 모두 시작되는 것을 확인하실 수 있습니다. 반면, 기존 이벤트의 경우 두 작업만이 시작되었다가 이벤트를 받고 두 작업이 종료된 후에나 다른 작업들이 시작되는 것을 확인하실 수 있습니다.
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

이제 본격적으로 VC++ 10의 병렬 프로그래밍에 대한 이야기를 시작합니다.

첫 번째는 이름만 들어도 딱 '병렬 프로그래밍' 이라는느낌을 주고 가장 많이 사용될 것으로 생각하는 Parallel Patterns Library (PPL)입니다정말 이름에서 딱 느낌이 오죠 ^^



PPL은 크게 세 개의 features로 나누어집니다.

1. Task Parallelism : 병렬적으로 여러 가지 작업 처리

2. Parallel algorithms : 데이터 컬렉션을 제너릭 알고리즘으로병렬 처리

3. Parallel containers and objects :concurrent 접근이 가능한 제너릭 컨테이너

 


PPL 모델은 C++의 Standard Template Library(STL)과비슷합니다.

예를 들면 STL에는 for_each 라는 것이 있는데 PPL에는 이것의 병렬 버전인 parallel_for_each가 있습니다. 뒤에 설명하겠지만 parallel_for_each에 대해서 간단하게 말하면 array의 항목을 순회하는 parallel 알고리즘입니다.



PPL을 사용하기 위해서는 먼저 namespace Concurrency를 선언한 후 ppl.h 파일을 포함합니다.
........
#include <ppl.h>

using namespace Concurrency;
..............


먼저 parallel_for_each를 사용한 코드를 보여 드리겠습니다. parallel_for_each는 다음에 자세히 설명하겠으니 이번은 PPL 이라는 것이 어떻게 사용하는지만 아래 코드를 통해서 보세요^^

< 리스트 1. parallel_for_each 예제 >

#include <ppl.h>

#include <array>

#include <algorithm>

 

using namespace std;

using namespace std::tr1;

using namespace Concurrency;

 

int main()

{

   // Create anarray object that contains a few elements.

   array<int, 3> a = {13, 26, 39};

 

   // Use thefor_each algorithm to perform an operation on each element

   // of the arrayserially.

  for_each(a.begin(), a.end(), [&](int n) {

      // TODO:Perform some operation on n.

   });

 

   // Use theparallel_for_each algorithm to perform the same operation

   // in parallel.

  parallel_for_each(a.begin(), a.end(), [&](int n) {

      // TODO:Perform some operation on n.

   });

}


<리스트 1>의 코드를 보면 람다를 사용한 부분도 보이죠? 예전에 제가 C++0x의 새로운 기능에 의해 C++의 성능과 표현력이 향상 되었다고 이야기 했습니다. 이런 장점들이 PPL에 많은 기여를 하였습니다.




PPL과 OpenMP

예전에 PPL이 MSDN 매거진을 통해서 공개 되었을 때 많은 분들이 OpenMP와 비슷하게 보시고 왜 기존에 있는 것과 같은 것을 또 만드냐 라는 이야기를 하는 것을 들은 적이 있습니다.

PPL과 OpenMP는 같은 것이 아닙니다. 표현 방법이 얼핏 비슷하게 보일지 몰라도 개념이나 기반은 많이 다릅니다.

OpenMP는 pragma 지신문이고 PPL은 순수 C++ 템플릿으로 만들어진 라이브러리입니다.
그래서 PPL은 표현성과 유연성이 OpenMP에서 비해서 훨씬 더 뛰어납니다.
또한 PPL은 Concurrency Runtime 기반 위에 구축되므로 동일한 런타임을 기반으로 하는 다른 라이브러리와 잠재적 상호 운용성이 제공됩니다.

PPL은 어떤 것인지, 왜 OpenMP 보다 더 좋은지 이후에 제가 적을 글을 보면 쉽게 알 수 있으리라 생각합니다.


오늘은 PPL의 개념에 대한 이야기로 마치고 다음에는 PPL의 하나인 task에 대해서 이야기 하겠습니다.
시간 여유가 있거나 task에 대해서 빨리 알고 싶은 분들은 일전에 정재원님이 task 예제를 설명한 글을 올린 적이 있으니 먼저 그것을 보면서 예습을 하는 것도 좋습니다.



저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Concurrency Runtime

VC++ 10 Concurrency Runtime 2009.07.30 06:00 Posted by 흥배

VSTS 2010 VC++ 10의 큰 핵심 feature 두 가지를 뽑으라고 하면 저는 C++0x와 Concurrency Runtime 두 가지를 뽑고 싶습니다.

VC++ 10
은 시대의 변화에 맞추어 새로운 C++ 표준과 병렬 프로그래밍을 받아들였습니다.

현재도 Win32 API에 있는 Thread  관련 API를 사용하여 병렬 프로그래밍을 할수 있습니다. 하지만 이것만으로 병렬 프로그래밍을 하기에는 너무 불편합니다.
그래서 VC++ 10에는 Concurrency Runtime 이라는 것이 생겼습니다.



Concurrency
Parallel의 차이


Concurrency는 병행, Parallel은 병렬이라고 합니다.

Concurrency는 독립된 요구를 동시에 처리하고, Parallel은 하나의 task를 가능한 Concurrency로 실행할 수 있도록 분해하여 처리합니다.

< 그림 출처 : http://blogs.msdn.com/photos/hiroyuk/picture9341188.aspx >


VSTS 2010에서는 Concurrency는 런타임 용어 Paralell은 프로그래밍 모델 용어가 됩니다.
이를테면 프로그래밍 때에 분해하여 런타팀에 넘기면(이것이 병렬화), 런타임은 그것을 Parallel로 실행합니다. Concurrency Runtime은 Parallel 런타임으로 이해하면 될 것 같습니다.




Concurrency Runtime

< 그림 출처 : http://blogs.msdn.com/photos/hiroyuk/picture9341189.aspx >

Cuncurrency Runtime은 C++ 병행 프로그래밍 프레임워크입니다. Cuncurrency Runtime복잡한 parallel code 작성을 줄여주고, 간단하게 강력하고, 확장성 있고 응답성 좋은 parallel 애플리케이션을 만듭니다. 또한 공통 작업 스케줄러를 제공하며 이것은 work-stealing 알고리즘을 사용하여 프로세싱 리소스를 증가시켜 애플리케이션의 확장성을 높여줍니다.

 


Cuncurrency Runtime에 의해 다음의 이점을 얻을 수 있습니다.

1. data parallelism 향상 : Parallel algorithms은 컬럭션이나 데이터 모음을 복수의 프로세서를 사용하여 배분하여 처리합니다.

2. Task parallelism : Task objects는 프로세서 처리에 독립적으로 복수 개로 배분합니다.

3. Declarative data parallelism : Asynchronous agents와 메시지 전달로 어떻게 실행하지 몰라도 계산을 선언하면 실행됩니다.

4. Asynchrony : Asynchronous agents는 데이터에 어떤 일을 처리하는 동안 기다리게 합니다.

 

 

Cuncurrency Runtime 컴포넌트는 네 가지로 나누어집니다.

1. Parallel Patterns Library (PPL)

2. Asynchronous Agents Library (AAL)

3. work scheduler

4. resource manager

 

이 컴포넌트는 OS와 애플리케이션 사이에 위치합니다.


< 그림 출처 : MSDN >


Cuncurrency Runtime의 각 컴포넌트는 아래의 네 개의 헤더 파일과 관련 되어집니다.

컴포넌트

헤더 파일

Parallel Patterns Library (PPL)

ppl.h

Asynchronous Agents Library (AAL)

agents.h

Concurrency Runtime work scheduler

concrt.h

Concurrency Runtime resource manager

concrtrm.h

 

 

Concurrency Runtime을 사용하기 위해서는  namespace Concurrency를 선업합니다.

Concurrency RuntimeC Runtime Library (CRT)를 제공합니다.


Concurrency Runtime의 대부분의 type와 알고리즘은 C++의 템플릿으로 만들어졌습니다. 또한 이 프레임워크에는 C++0x의 새로운 기능이 많이 사용되었습니다.

대부분의 알고리즘은 파라메터 루틴을 가지고 작업을 실행합니다. 이 파라메터는 람다 함수, 함수 오브젝트, 함수 포인터입니다.



처음 들어보는 단어를 처음부터 막 나오기 시작해서 잘 이해가 안가는 분들이 있지 않을까 걱정이 되네요. 그래서 핵심만 한번 더 추려 보겠습니다.^^

1. Concurrency는 병행, Parallel은 병렬.
2. VSTS 2010에서는 Concurrency는 런타임 용어로 Paralell은 프로그래밍 모델 용어.
3. 프로그래밍 때에 분해하여 런타팀에 넘기면(이것이 병렬화), 런타임은 그것을 Parallel로 실행.
4. Cuncurrency Runtime은 C++ 병행 프로그래밍 프레임워크로 복잡한 parallel code 작성을 줄여주고, 간단하게 강력하고, 확장성 있고 응답성 좋은 parallel 애플리케이션을 만들수 있으며 공통 작업 스케줄러를 제공하며 이것은 work-stealing 알고리즘을 사용하여 프로세싱 리소스를 증가시켜 애플리케이션의 확장성을 높여준다.

5. Cuncurrency Runtime 컴포넌트는 네 가지로 나누어진다.

  1. Parallel Patterns Library (PPL)

  2. Asynchronous Agents Library (AAL)

  3. work scheduler

  4. resource manager



그럼 다음에는 Parallel Patterns Library(PPL)에 대해서 이야기 하겠습니다.^^





저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
일곱,여덟번째로 드디어 마지막입니다. ㅠㅠ

역시나 Q&A가 이어지고, 끝에 선물을 주네요.




http://channel9.msdn.com/pdc2008/TL25/

그 동안 시청해주셔서 감사합니다. 조만간 새로운 내용으로 다시 돌아오겠습니다. ^^

p.s. 필요하다는 분이 계서서 이제까지의 자막 파일들 첨부합니다.


신고
크리에이티브 커먼즈 라이선스
Creative Commons License
여섯번째입니다.

이미징 데모 설명과 시연을 끝내고 전체 요약 후 질답 세션으로 넘어갑니다.


http://channel9.msdn.com/pdc2008/TL25/

혹 자막이 안보이시는 분은 유투브 플레이어에서 CC(Turn on Captions)를 활성화해주시면 보일겁니다.
(임베드 태그에서 기본으로 보이도록 설정했는데 왜 기본으로 안나오는지 모르겠군요;)
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
다섯번째입니다. 이제 끝이 보이네요;
많이 늦었죠? 죄송합니다. 출장 때문에 한 2주동안 바빴습니다.

이번 회에서는 Asynchronous Agents Library에 대해 더 자세히 살펴보고 실제 적용 예를 보여줍니다.

http://channel9.msdn.com/pdc2008/TL25/

그 사이 VS2010 베타1의 발표와 함께 많을 자료들이 쏟아져 나왔는데요. AAL 관련해서도 유용한 글들이 보이더군요.



AAL로 액터 기반 병렬 프로그래밍을 경험해보세요! 
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
네번째입니다.

Combinable에 대해 자세히 다루고 PPL을 마무리한 뒤, Asynchronous Agents Library가 소개됩니다.


잘못된 번역이나 부족한 부분 있으면 알려주세요. ^^
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
세번째입니다.

관련 디버깅 유틸과 PPL 지원 알고리즘, 이미징 예제 병렬화, 동기화 개체자료구조 등이 소개됩니다.


잘못된 번역이나 부족한 부분 있으면 알려주세요. ^^
신고
크리에이티브 커먼즈 라이선스
Creative Commons License


 

티스토리 툴바