지난 포스팅에서 Break와 Stop메서드의 차이에 대해서 시덥잖은 퀴즈를 남겼었는데욤, 뭐 별로 궁금한 분들이 없거나 너무 쉬워서 코에서 바람 좀 내뿜은 분들이 많으리라 생각합니다. 저는 이게 뭔 차이인지 잠깐 헤맸습니다만... 허허허 :) 그럼 차이점에 대해서 잠깐 이야기를 해볼까욤.


- Parallel.For와 for의 브레끼는 닮았더라.

Parallel.For의 Break메서드는 for의 break와 좀 닮았습니다. for루프에서 break를 만나게 되면 그 즉시 for루프를 중단하게 됩니다. 그리고 break가 걸리기 전 까지의 인덱스에 대해서는 루프가 실행되었음을 보장할 수 있습니다. for루프는 처음부터 순차적으로 실행되기 때문이죠. 그런데 Parallel.For에서는 루프문의 길이에 대해서 각각의 스레드가 조금씩 나눠서 담당하게 됩니다. 예를 들면 1-10000까지의 구간이라면, 스레드 하나가 1-5000까지, 다른 스레드가 5001-10000까지 뭐 이런식으로 말이죠. 이런 상황에서 5100번째 인덱스에서 Break가 호출된다면 어떻게 될까요? 기존의 for루프를 사용하던 관점에서 보자면 당연히 5109번째 인덱스까지는 다 처리되었을 거라고 예상하게 됩니다. 이런 개발자의 기대를 저버리지 않기 위해서 Break메서드를 호출하게 되면 Break가 호출된 인덱스보다 작은 인데스에 대해서 남아있는 작업은 모두 실행하도록 합니다. 예제를 볼까요?

using System;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Parallel.For(0, 20, (i, loopState) =>

            {

                if (i == 10)

                {

                    loopState.Break();

                    Console.WriteLine("Break at {0}", i.ToString());

                }

                else

                {

                    Console.WriteLine(i.ToString());

                }

            });

        }

    }

}


아주 간단하게 0부터 19까지 Parallel.For루프를 돌면서 i가 10일때 Break를 호출해서 루프를 중단하도록 하는 코드입니다. 실행할 때마다 결과가 다르게 나오는데요, 그 중에 가장 설명하기 좋은 결과를 고르면!

0

1

2

3

4

6

5

Break at 10

7

8

9

Press any key to continue . . .


대략 이렇습니다. 스레드가 구간을 나눠서 루프를 실행하는데, 먼저 실행한 스레드가 0-6까지를 처리하고 다른 스레드가 10을 처리하는 순간 Break를 만나게 되는 거죠. 하지만 여기서 중단 되지 않고, 다른 스레드가 7, 8, 9를 처리하도록 해줍니다. 그래서 10에서 Break가 호출되었더라도 바로 중단하지 않고 10보다 작은 인덱스가 모두 처리되도록 해주는 것이죠.


- 장비를 정지합니다... 어 안되잖아?

Break를 즉시 중단하는 개념으로 생각하다 보면 조금은 이상할 수 있습니다만, 사실 기존의 for루프와 유사한 동작을 제공한다는 개념에서는 바람직한 동작이 아닌가 싶습니다. 그런데, 데이터에서 뭔가를 검색하는 경우는 어떨까요? 4명이 근무하는 사무실을 생각해봅시다. 뭔가 중요한 서류를 찾아야 해서 사장 이하 4명이 구역을 나눠서 사무실을 뒤지기 시작합니다. 한 명은 책상을, 한 명은 캐비넷을, 또 한 명은 창고를, 또 한 명은 잉여짓을. 이렇게 4명이 찾다가 누군가 한 명이 그 서류를 발견했습니다!!! 그러면 그 사람은 '심봤다 어허으어릉러허엏엉 나 심봤쪄영 뿌우 'ㅅ'~'하고 외치겠죠? 그러면 그 사람들은 그 찾는 작업을 멈추게 됩니다. 그게 정상이겠죠. '그럴리가 없엉 어흐어흐어흐러라어헝'하면서 나머지를 계속 찾는 사람은 '안돼에~'. 허허허허 :)

이런 경우에는 Parallel.For를 즉시 중시해야 합니다. 그럴때! 바로 Stop을 호출하게 됩니다.

using System;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Parallel.For(0, 20, (i, loopState) =>

            {

                if (i == 10)

                {

                    loopState.Stop();

                    Console.WriteLine("Stop at {0}", i.ToString());

                }

                else

                {

                    Console.WriteLine(i.ToString());

                }

            });

        }

    }

}

Break대신에 Stop을 호출한 코드죠. 결과능!

0

5

Stop at 10

Press any key to continue . . .


짤 없습니다. 그냥 중단해버리는 거죠. 자 지난 포스팅에서 왜 Stop을 호출한 게 미세하게 빨랐는지 아시겠나요? 소수는 1과 자신 외에 하나라도 나눌 수 있는 숫자가 존재하면 안되는 거죠. 그래서 for루프에서 숫자를 바꿔가면서 나눠보다가 하나라도 나눠지는 게 발견되면 그 즉시 중단하는 게 맞는 거죠. Break는 발견되더라도, 그 보다 작은 인덱스에 대해서는 모두 수행하게 되기 때문에 Stop에 비해서 아주 미세하게 느린 결과가 나오게 되는 거죵. 허허허허허 :) 참 별 내용 없네요 ㅠㅠ...

그럼 오늘은 여기까징~!

Welcome to Parallel C#(14) - 거기까지.

C# Parallel Programming 2012. 2. 13. 09:00 Posted by 알 수 없는 사용자

자~ 그럼 이제 부터는 다시 병렬 프로그래밍으로 돌아가서 조금 이야기를 해보도록 할 까욤~? 오래전에 써놓은 자료를 보다보니..... 제가 쓴 내용이지만 무슨 내용인지 이해가 안되는 글이 많아서 다시 정리를 하느라 조금 애를 먹고 있습니다. 하하하하 :) 뭐 제 수준이 딱 거기 까지니까 말이죵 호호호호.


- 병렬 작업을 중단할 때능?

기존의 for루프를 중단할 때는 그냥 간단하게 break하나 추가해넣으면 되었습니다. for루프는 정해진 순서에 따라서 순차적으로 실행되기 때문에 특정 조건에서 break를 만나서 루프가 중단 된다고 하더라도 항상 취소되기 이전까지의 내용은 모두 실행이 되었음을 보장할 수 있었죠. 즉,

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

{

    if (i == 50)

    {

        break;

    }

}


위와 같은 코드가 있다고 할 때, i가 50이되어서 for루프가 중단된다고 할 때, i가 0-49일때는 이미 다 진행된 상태라는 것이죠. 그런데 이런 루프를 빠르게 처리하기 위해서 여러 스레드를 동시에 사용하는 Parallel.For를 사용했다고 한다면... 각 스레드가 나눠서 진행중인 작업을 어떻게 중단 시켜야 할까요?


- 브레이크를 걸자.

일단은 기존의 for루프와 가장 유사한 형태를 보이는 것 부터 살펴봅시당. 일단 비교를 위해서 다음과 같은 예제를 먼저 살펴보겠습니다.

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            for (long i = 0; i <= num; i++)

            {

                bool isPrime = true;

                for (long j = 2; j < i; j++)

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        break;

                    }

                }

                if (isPrime)

                {

                    primeList.Add(i);

                }

            }

 

            return primeList;

        }

       

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


아주 간단한 코드인데요, 1부터 사용자가 입력한 숫자 중에서 소수를 구해서 리스트에 추가하는 코드입니다. 그리고 각 수가 소수인지 검사하는 부분에서 1이 아닌 수로 나눠지는 케이스가 발견 되면 그 즉시 break를 이용해서 중단을 시키고 있죠. 위 코드를 실행하면 CPU점유율은 아래와 같이 단일 스레드를 이용하는 것을 볼 수 있습니다.


그리고 위 코드를 3번 실행해서 얻은 평균 실행 시간은 6.28초 입니다. 그렇다면 위 코드의 Parallel.For버전을 살펴 볼까욤?

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            Parallel.For(0, num + 1, (i) =>

            {

                bool isPrime = true;

                Parallel.For(2, i, (j, loopState) =>

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        loopState.Break();

                    }

                });

 

                if (isPrime)

                {

                    primeList.Add(i);

                }

            });

 

            return primeList;

        }

       

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


for를 Parallel.For로 바꿨고, break역시 Parallel.For의 인자로 넘어오는 loopState변수에 대해서 Break메서드를 호출하는 것으로 변경되었습니다. 이 코드를 실행해보면, 아래와 같이 CPU의 4개의 코어가 모두 동작하는 것을 볼 수 있습니다.




그리고 역시 위 코드를 3번 실행해서 얻은 평균 실행 시간은 5.32 초입니다. 전에 비해서 약 15%의 성능향상이 있었습니다. 물론 소수를 구하는 알고리즘 자체를 더 개선할 수도 있지만, 단순히 싱글 스레드를 이용한 코드와 작업을 병렬화 시켜서 멀티 코어를 이용한 코드간의 차이가 이정도라면 의미가 있지 않을까요? 그리고 좀더 CPU를 많이 사용하는 코드일 수록 그 차이는 더 벌어지겠죠 :)


- 그런데 사실은...

위 코드에서 몇자를 바꾸고 나면, 코드의 평균 실행 시간이 5.29초로 조금 더 떨어지게 됩니다. 그 코드를 올려드릴 테니 어디를 수정한 건지 한번 찾아보시죵 :)

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            Parallel.For(0, num + 1, (i) =>

            {

                bool isPrime = true;

                Parallel.For(2, i, (j, loopState) =>

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        loopState.Stop();

                    }

                });

 

                if (isPrime)

                {

                    primeList.Add(i);

                }

            });

 

            return primeList;

        }

        

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


어딘지 찾으셨나요? 바로 loopState변수에 대해서 Break를 호출하던 것을 Stop으로 바꿔준 것입니다. 그래서 조금 더 효율적인 코드가 되는 것이죠. 왜 그럴까요? 그거에 대해서는!!!! 다음 포스팅에서 ㅋㅋㅋㅋㅋ


- 참고자료
http://stackoverflow.com/questions/1510124/program-to-find-prime-numbers