Parallel Patterns Library(PPL) - combinable

VC++ 10 Concurrency Runtime 2009. 10. 28. 08:30 Posted by 알 수 없는 사용자

PPL에서 제공하는 알고리즘을 사용하여 병렬로 작업을 실행할 때 각 작업에서 접근하는 공유 리소스는 스레드 세이프 하지 않기 때문에 lock을 걸어서 공유 리소스를 보호해야 합니다.

 

그러나 lock을 건다는 것은 번거롭기도 하며 성능에 좋지 않은 영향을 미칩니다.

가장 좋은 방법은 공유 리소스에 lock을 걸지 않아도 스레드 세이프한 것이 가장 좋습니다.

 

combinable은 바로 위에 언급한 문제를 해결해 주는 것입니다. 모든 상황에 다 사용할 수 있는 것은 아니지만 특정 상황에서는 combinable을 사용하면 lock을 걸지 않아도 공유 리소스를 스레드 세이프하게 접근 할 수 있습니다.

 

 

combinable

combinable은 병렬로 처리하는 작업에서 각 작업마다 계산을 실행한 후 그 계산 결과를 통합할 때 사용하는 재 사용 가능한 스레드 로컬 스트레지를 제공합니다.

 

combinable은 복수의 스레드 또는 태스크 간에 공유 리소스가 있는 경우에 사용하면 편리합니다. combinable는 공유 리소스의 접근을 각 스레드 별로 제공하여 공유 상태를 제거할 수 있습니다.

 


스레드 로컬 스트리지

스레드 프로그래밍을 공부하시면 스레드 고유의 로컬 스트리지를 만들어서 해당 스레드는 자신의 로컬 스트리지에 읽기,쓰기를 하여 다른 스레드와의 경합을 피하는 방법을 배울 수 있습니다.

combinable은 이 스레드 로컬 스트리지와 비슷한 방법입니다.

 

 

combinable의 메소드 설명

combinable::local : 현재 스레드 컨텍스트와 관련된 로컬 변수의 참조를 얻는다.

combinable::clear : 오브젝트로부터 모든 스레드 로컬 변수를 삭제한다.

combinable::combine : 제공하고 있는 있는 조합 함수를 사용하여 모드 스레드 로컬 계산의 set으로부터 최종적인 값을 만든다.

combinable::combinable_each ; 제공하고 있는 조합 함수를 사용하여 모든 스레드 로컬 계산의 set으로부터 최종적인 값을 만든다.

 

 

combinable은 최종 결합 결과 타입의 파라미터를 가지고 있는 템플릿 클래스입니다. 기본 생성자를 호출하면 기본 생성자와 복사 생성자 _Ty 템플릿 파라미터 형이 꼭 있어야합니다. _Ty 템플릿 파라미터 형이 기본 생성자를 가지지 않는 경우 파라미터로 초기화 함수를 사용하는 생성자로 오버로드 되어 있는 것을 호출합니다.

 

combinable을 사용하여 모든 작업에서 처리한 계산 결과 값을 얻을 때는 combine()을 사용하여 합계를 구하던가, combine_each를 사용하여 각 작업에서 계산한 값을 하나씩 호출하여 계산합니다.

 

< 예제 1. Combinable을 사용하지 않고 lock을 사용할 때 >

……

int TotalItemPrice1 = 0;

critical_section rt;

parallel_for( 1, 10000, [&]( int n ) {

                     rt.lock();

                     TotalItemPrice += n;

                     rt.unlock();

                     }         

);

………


<예제 1>critical_section을 사용하여 TotalItemPrice 변수를 보호하고 있습니다.

그럼 <예제 1> combunable을 사용하여 구현해 보겠습니다.

 

< 예제 2. Combinable 사용 >

#include <ppl.h>

#include <iostream>

 

using namespace Concurrency;

using namespace std;

 

 

int main()

{

           combinable<int> ItemPriceSum;

           parallel_for( 1, 10000, [&]( int n ) {

                                ItemPriceSum.local() += n;

                                }         

                     );

 

           int TotalItemPrice = ItemPriceSum.combine( [](int left, int right) {

                                          return left + right;}

                                );

 

           cout << "TotalItemPrice : " << TotalItemPrice << endl;

          

          

           getchar();

           return 0;

}

 

combinable을 사용하면 <예제 1>과 다르게 lock을 걸지 않아도 되기 때문에 훨씬 성능이 더 좋습니다. 다만 모든 곳에서 사용할 수는 없기 때문에 <예제 2>와 같이 어떤 계산의 최종 결과를 구할 때 등 사용할 수 있는 곳을 잘 찾아서 사용해야 합니다.

 

<예제 2>는 각 태스크에서 계산된 결과를 더하기 위해서 conbinablecombine 멤버를 사용했지만 각 태스크의 결과를 하나씩 순회할 때는 conbinablecombine _each 멤버를 사용합니다.

그리고 저는 <예제 2>에서 int combinable에 사용했지만 int 이외에 유저 정의형이나 STL list와 같은 컨테이너도 사용할 수 있습니다.

 


combinable에서 combine_each() 멤버나 combinable에서 STL list 컨테이너를 사용한 MSDN에 있는 예제는 아래와 같습니다.

#include <ppl.h>

#include <vector>

#include <list>

#include <algorithm>

#include <iostream>

 

using namespace std;

using namespace Concurrency;

 

int main()

{

   // Create a vector object that contains the values 1 through 10.

   vector<int> values(10);

  

   int n = 0;

   generate(values.begin(), values.end(), [&] { return ++n; } );

 

   // Generate the list of odd elements of the vector in parallel

   // by using the parallel_for_each algorithm and a combinable object.

   combinable<list<int>> odds;

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

         if (n % 2 == 1)

            odds.local().push_back(n);

       });

 

   // Combine all thread-local elements into the final result.

   list<int> result;

   odds.combine_each([&](list<int>& local) {

           // Merge the local list into the result so that the results

           // are in numerical order.

           local.sort(less<int>());

           result.merge(local, less<int>());

        });

 

   // Print the result.

   cout << "The odd elements of the vector are:";

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

          cout << ' ' << n;

        });

}


Parallel Patterns Library(PPL) - parallel_invoke

VC++ 10 Concurrency Runtime 2009. 10. 20. 08:30 Posted by 알 수 없는 사용자

parallel_invoke는 일련의 태스크를 병렬로 실행할 때 사용합니다. 그리고 모든 태스크가 끝날 때까지 대기합니다. 이 알고리즘은 복수의 독립된 태스크를 실행할 때 유용합니다.

 

일련의 태스크를 병렬로 실행할 때 사용이라는 것을 들었을 때 생각나는 것이 없는가요? 지금까지 제가 올렸던 글을 보셨던 분이라면 parallel task라는 말이 나와야 합니다. ^^

parallel_invoke parallel task와 비슷합니다.

 

 

parallel_invoke parallel task의 다른 점

복수 개의 태스크를 병렬로 실행한다는 것은 둘 다 같지만 아래와 같은 차이점이 있습니다.


 

parallel_invoke

parallel task

편이성

작업 함수만 정의하면 된다.

작업 함수를 만든 후 task handle로 관리해야 한다.

태스크 개수

10개 이하만 가능

제한 없음

모든 태스크의 종료 시 대기

무조건 모든 태스크가 끝날 때까지 대기

Wait를 사용하지 않으면 대기 하지 않는다.



parallel_invoke를 사용할 때

병렬로 실행할 태스크의 개수가 10개 이하이고, 모든 태스크가 종료 할 때까지 대기해도 상관 없는 경우에는 간단하게 사용할 수 있는 parallel_invoke를 사용하는 것이 좋습니다. 하지만 반대로 병렬로 실행할 태스크가 10개를 넘고 모든 태스크의 종료를 대기하지 않아야 할 때는 parallel task를 사용해야 합니다.

 

 

parallel_invoke 사용 방법

parallel_invoke는 병렬로 태스크를 두 개만 실행하는 것에서 10개까지 실행하는 9개의 버전이 있으며 파라미터를 두 개만 사용하는 것에서 10개의 파라미터를 사용하는 것으로 오버로드 되어 있습니다.

각 오버로드된 버전의 파라미터에는 태스크를 정의한 작업 함수를 넘겨야 합니다.

 

 

parallel_invoke 사용 예

아래 예제는 아주 간단한 것으로 게임 프로그램이 처음 실행할 때 각종 파일을 로딩하는 것을 아주 간략화 하여 parallel_invoke를 사용한 예입니다.

 

#include <iostream>

#include <ctime>

#include <windows.h>

#include <concrt.h>

#include <concrtrm.h>

using namespace std;

 

#include <ppl.h>

using namespace Concurrency;

 

// UI 이미지 로딩

void LoadUIImage()

{

           Sleep(1000);

           cout << "Load Complete UI Image" << endl;

}

 

// 텍스쳐 로딩

void LoadTexture()

{

           Sleep(1000);

           cout << "Load Complete Texture" << endl;

}

 

// 폰트 파일 로딩

void LoadFont()

{

           Sleep(1000);

           cout << "Load Complete Font" << endl;

}

 

int main()

{

           parallel_invoke( [] { LoadUIImage(); },

                      [] { LoadTexture(); },

                      [] { LoadFont(); }

                    );

          

           getchar();

           return 0;

}

 

< 실행 결과 >



위 예제를 parallel_invoke를 사용하지 않고 전통적인 방법으로 순서대로 실행했다면 각 작업 함수에서 1초씩 소비하므로 3초가 걸리지만 parallel_invoke를 사용하여 1초만에 끝납니다.

 

그리고 이전에 parallel_for에서도 이야기 했듯이 병렬로 실행할 때는 순서가 지켜지지 않는다는 것을 꼭 유의하시기 바랍니다. 위의 예의 경우도 LoadUIImage()을 첫 번째 파라미터로 넘겼지만 실행 결과를 보면 LoadFont()가 먼저 완료 되었습니다.

 


마지막으로 위의 예제코드에서 parallel_invoke와 관계 있는 부분만 추려볼 테니 확실하게 사용 방법을 외우시기를 바랍니다.^^

 

#include <ppl.h>

using namespace Concurrency;

 

// 태스크 정의

void LoadUIImage()

{

............

}

 

void LoadTexture()

{

............

}

 

void LoadFont()

{

............

}

 

int main()

{

        parallel_invoke( [] { LoadUIImage(); },

                                 [] { LoadTexture(); },

                                 [] { LoadFont(); }

                          );

 

}


About Visual C++ 10

Visual C++ 10 2009. 10. 15. 08:30 Posted by 알 수 없는 사용자

지난 10 7일에서 9일까지 서울 코엑스에서 ‘KGC 2009’ 라는 게임 개발 컨퍼런스가 열렸습니다.

저는 여기서 ‘Visual C++ 10’에 대해서 강연을 했습니다.

 

이번 버전 10은 이전에 비해서 변화한 부분이 많아서 가장 큰 핵심인 ‘C++0x’‘Parallel’부분만 주제를 잡았는데도 시간 부족상 다 하지는 못했습니다.

 

아직 VSTS 2010 Beta1을 설치하지 않은 분들도 많으실 것 같아서 Demo까지 하려니 도저히 시간이 남지 않더군요.

 

이번에 제대로 하지 못한 ‘Parallel’ 부분은 꼭 다음에 기회가 되는대로 강연을 하도록 하겠습니다.^^





ps : 근래 바빠서 블로그에 포스팅을 하지 못했는데 곧 새로운 글을 올리겠습니다.



C++ STL을 알고 있는 분들은 ‘parallel_for_each’에서 ‘parallel_’만 빼면 남는 ‘for_each’는 본적이 있고 사용해본 경험도 있을 것입니다.

 

parallel_for가 for문을 병렬화 한 알고리즘이라면 parallel_for_each는 STL의 for_each 알고리즘을 병렬화 한 것입니다.

 

STL 컨테이너에 있는 데이터를 처리할 때 for_each를 사용한 것을 쉽게 parallel_for_each로 바꾸어 아주 손 쉽게 병렬화 할 수 있습니다.

 

 

parallel_for_each의 원형

 

template < typename _Input_iterator, typename _Function >

_Function parallel_for_each( _Input_iterator _First,  _Input_iterator _Last,   _Function _Func );

 

_First : 시작 위치

_Last : 마지막 위치

_Func : 병렬 처리에 사용할 함수(함수 객체, 함수, 람다 식)

 

for_each에 대해서 알고 있는 분들은 앞서 소개한 parallel_for 보다 더 쉽다고 느낄 것입니다. 기존의 for_each가 사용하는 파라미터도 같습니다. 기존에 사용했던 for_each parallel_for_each로 바꿀려면 알고리즘 이름만 바꾸어도 됩니다.

 

 

 

초 간단 parallel_for_each 사용 방법

 

1. 필요한 헤더 파일 포함

#include <ppl.h>


2.네임 스페이스 선언

using namespace Concurrency;

 

3. parallel_for_each에서 사용할 함수 정의

 

4. parallel_for_each에서 사용할 STL 컨테이너 정의

 

5. parallel_for_each 사용

 

 

 

parallel_for_each를 사용하는 간단한 예제


#include <iostream>

#include <algorithm>

#include <vector>

using namespace std;

 

#include <ppl.h>

using namespace Concurrency;

 

int main()

{

     vector< int > ItemCdList(10);

     generate( ItemCdList.begin(), ItemCdList.end(), []() -> int {

                                       int n = rand();

                                       return n; }

              );

 

      cout << "for_each" << endl;

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

                            cout << "<" << n << ">"; } );

      cout << endl << endl;

 

      cout << "parallel_for_each - 1" << endl;

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

                                    cout << "<" << n << ">"; }

                        );

      cout << endl << endl;

 

      cout << "parallel_for_each - 2" << endl;

      critical_section rt;

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

                               rt.lock();

                              cout << "<" << n << ">";

                               rt.unlock(); }

                       );

 

      getchar();

      return 0;

}

 


위 예제는 vecter 컨테이너에 램덤으로 10개의 숫자 값을 채운 후 출력하는 것입니다.


for_each paralle_for_each 사용 방법이 이름만 다를 뿐 똑 같습니다.




위 예제를 초 간단 parallel_for_each 사용 방법의 순서에 비추어 보면 아래 그림과 같습니다.

 

 

위 예제의 결과입니다.

 



공유 자원 동기화 문제


parallel_for 때도 잠시 언급했듯이 parallel_for_each는 순서대로 실행하지 않고 병렬로 실행하므로 for_each를 사용한 것과 비교해 보면 출력 순서가 서로 다릅니다.

그리고 특히 문제가 되는 것이 공유 자원을 사용할 때 따로 동기화 시키지 않으면 원하지 않는 결과가 나옵니다.

 



위와 같은 잘못된 결과는 나올 수도 있고 안 나올 수도 있습니다. 즉 타이밍에 의해서 발생하는 것이기 때문입니다. 이것이 병렬 프로그래밍의 어려움 중의 하나인데 에러가 언제나 발생하면 빨리 발견하여 처리할 수 있는데 공유 자원을 동기화 하지 않았을 때 발생하는 문제는 바로 발생할 수도 있고 때로는 여러 번 실행했을 때 간혹 나올 때도 있어서 버그 찾기에 어려움이 있습니다.

 

공유 자원의 동기화가 깨어지는 것을 막기 위해서는 동기화 객체를 사용하면 됩니다. 위 예제에서 두 번째 사용한 parallel_for_each‘critical_section’이라는 동기화 객체를 사용하여 공유 자원을 안전하게 보호하고 있어서 올바르게 값을 출력하고 있습니다.

‘critical_section’에 대해서는 다음 기회에 자세하게 설명하겠습니다.

 

parallel_for_each에 대해서는 이것으로 마무리하고 다음 번에는 parallel_invoke에 대해서 설명하겠습니다.

 


원래 저번 주에 글을 올릴 예정이었으나 근래에 제 몸 상태와 집 PC 상태가 메롱이 되어버려 한 주 늦게 글을 올립니다(혹시 기다리고 계시는 분이 있었는지 모르겠네요 ^^;;; )



for 문의 병렬화 

이번에는 PPL의 세 개의 알고리즘 중 parallel_for 알고리즘에 대해서 이야기 하겠습니다.

앞 글에서 간단하게 설명했듯이 parallel_for는 그 이름을 보면 유추 할 수 있듯이 for 문을 병렬화 한 알고리즘입니다.

 

아주 많은 횟수로 반복 작업을 해야할 때 하나의 스레드로 처리하는 것보다는 여러 스레드로 동시에 처리하면 훨씬 빨라지는 것은 당연하겠죠? 바로 이 때 사용하면 좋습니다.

하지만 parallel_for 알고리즘은 아무 곳에나 사용할 수는 없습니다. 루프의 반복 계산 사이에 리소스를 공유하지는 않으면서 루프의 본체가 있는 경우 사용하면 편리합니다.

( 앞의 계산 결과를 다음 계산에서 사용해야 된다면 병렬로 실행하기 힘듭니다 )

 

 

parallel-for의 원형

 

두 개의 오버로드 버전이 있습니다.

 

template < typename _Index_type, typename _Function >

_Function parallel_for( _Index_type _First,  _Index_type _Last, _Function _Func );

_Index_type _First : 시작 위치

_Index_type _Last : 마지막 위치

_Function _Func : 병렬 처리로 사용할 함수

 

 

template < typename _Index_type, typename _Function >

_Function parallel_for( _Index_type _First, _Index_type _Last, _Index_type _Step, _Function _Func );

_Index_type _First : 시작 위치

_Index_type _Last : 마지막 위치

_Index_type _Step : 증분 값

_Function _Func : 병렬 처리로 사용할 함수

 

파라미터 값을 보면 for에서 사용하는 것과 비슷하다는 것을 알 수 있을겁니다. 차이점은 첫 번째 버전의 경우 증분 값으로 1이 자동으로 사용된다는 것과 마지막 파리미터로 병렬 처리에 사용할 함수를 사용한다는 것입니다.

 

 

for와 비슷하므로 for를 사용하는 대 부분을 prarallel_for로 변경할 수 있습니다. 다만 parallel_for 알고리즘에서는 반복 변수의 현재 값이 _Last 보다 작으면 중단합니다 ( 보통 for 문과 다르게 ‘<’ 조건만 사용합니다 ).

또 _Index_type 입력 파라미터는 정수형이어야만 합니다.

parallel_for 파라미터가 1보다 작은 경우 invalid_argument_Step 예외를 던집니다.

 


 

초 간단 parallel_for 사용 방법

 

1. 필요한 헤더 파일 포함
  #include <ppl.h>


2.
네임 스페이스 선언

  using namespace Concurrency;

 

3. parallel_for에서 호출할 작업 함수 정의

 

4. parallel_for에서 사용할 data set 정의

 

5. parallel_for 사용

 

 

 그럼 아주 간단한 실제 사용 예제 코드를 볼까요?

 

#include <ppl.h>

#include <iostream>

 

using namespace Concurrency;

using namespace std;

 

 

int main()

{

    int CallNum = 0;

    int Numbers[50] = { 0, };


   
parallel_for( 0, 50-1, [&](
int n ) {

        ++CallNum;

        Numbers[n] += CallNum;

       }               

      );

 

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

    {

        cout << i << " : " << Numbers[i] << endl;

    }

 

    getchar();

    return 0;

}


 

위 예제는 Numbers라는 int 형 배열의 각 요소에 CallNum 이라는 변수를 더하는 것입니다. 간단하고 확실하게 parallel_for 사용 방법을 보이기 위해 허접한 예제를 만들게 되었음을 양해 바랍니다.^^;;; ( 다음에 기회가 되면 좀 더 멋지고 실용적인 예제를 보여드리도록 하겠습니다 )

예제에서는 코드를 간략화 하기 위해서 parallel_for의 마지막 파리미터로 람다 식을 사용했습니다.

위 예제를 '초 간단 parallel_for 사용 방법'의 순서에 비추어보면 아래 그림과 같습니다.

 

 


예제를 실행하면 아래와 같은 결과가 나옵니다.

 

(길어서 일부만 캡쳐 했습니다)

 

실행 결과를 보면 Numbers 배열의 각 요소의 값이 순서대로 증가되지 않았다라는 것을 알 수 있습니다. 만약 보통의 for 문이라면 Numbers[0] 1, Numbers[1] 2 라는 값으로 됩니다. 그러나 parallel_for는 병렬적으로 실행되므로 순서가 지켜지지 않습니다. CallNum 라는 변수는 parallel_for의 모든 스레드에서 접근하는 공유 변수이므로 동기화 되지 않았다라는 것도 유의해야 합니다.

 

Parallel_for를 사용할 때 순서대로 실행하지 않고, 공유 변수는 동기화 되지 않음을 잊지마시기를 바랍니다.

 

이것으로 (너무?)간단하게 parallel_for에 대해서 알아 보았습니다. 다음에는 parallel_for_each에 대해서 설명하겠습니다.




수정

1. 덧글의 ivyfore님이 알려주신대로

parallel_for( 0, 50-1, [&]( int n )가 아닌

 parallel_for( 0, 50, [&]( int n ) 가 되어야 합니다.

Parallel Patterns Library(PPL) - 병렬 알고리즘

VC++ 10 Concurrency Runtime 2009. 8. 19. 13:00 Posted by 알 수 없는 사용자

Parallel Patterns Library(이하 PPL)에는 데이터 컬렉션을 대상으로 쉽게 병렬 작업을 할 수 있게 해 주는 알고리즘이 있습니다. 이 알고리즘들은 생소한 것들이 아니고 C++의 표준 템플릿 라이브러리(STL)에서 제공하는 알고리즘과 비슷한 모양과 사용법을 가지고 있습니다.

( *데이터 컬렉션은 데이터 모음으로 배열이나 STL 컨테이너를 생각하면 됩니다 )

 

 

PPL에서 제공하는 병렬 알고리즘은 총 세 개가 있습니다.

 

1. parallel_for        알고리즘

2. parallel_for_each 알고리즘

3. parallel_invoke    알고리즘

 

 

세 개의 알고리즘 중 3 parallel_invoke만 생소하지 1번과 2번은 앞의 ‘parallel_’이라는 글자만 빼면 ‘for’‘for_each’ C++로 프로그래밍할 때 자주 사용하는 것이므로 친숙하게 느껴질 겁니다.

실제 병렬 여부만 제외하면 우리가 알고 있는 것들과 비슷한 동작을 합니다. 그래서 쉽게 배울 수 있고 기존의 코드에 적용하기도 쉽습니다.

 


parallel_for 알고리즘은 일반적인 for문을 사용할 때와 비슷하게 데이터 컬렉션에서 시작할 위치와 마지막 위치, 증가분(생략 가능합니다)에 해야할 작업 함수를 파라미터로 넘기면 됩니다. 사용 방법에서 for문과 다른 점은 작업 함수를 넘긴다는 점입니다.

 

parallel_for_each 알고리즘은 기존 for_each와 거의 같습니다. 데이터 컬렉션에서 시작할 위치, 마지막 위치, 작업 함수를 파라미터로 넘기면 됩니다. parallel_for의 경우 기존의 for문을 사용할 때는 작업 함수를 파라미터로 넘기지 않기 때문에 기존 for 문에 비해서 구조가 달라지지만 parallel_for_each는 기존 for_each와 파라미터 사용 방법이 같기 때문에 알고리즘의 이름만 바꾸면 될 정도입니다.

 

parallel_invoke 알고리즘 이전 회에 설명한 태스크 그룹과 비슷한면이 있습니다. 태스크 그룹과의 큰 차이점은 병렬로 할수 있는 작업은 10개로 제한 되지만 사용 방법은 태스크 그룹보다 더 간결한 점입니다다. 병렬 작업의 개수가 10개 이하인 경우 태스크 그룹보다 parallel_invoke를 사용하는 것이 훨씬 더 적합하다고 생각합니다.

 

 

 

 

이번은 간단하게 PPL에 있는 세 가지 병렬 알고리즘을 소개하는 것으로 마칩니다. 다음 회부터는 이번에 소개했던 세 개의 알고리즘을 하나씩 하나씩 자세하게 설명하겠습니다.

Parallel Patterns Library(PPL) - Task

VC++ 10 Concurrency Runtime 2009. 8. 18. 00:27 Posted by 알 수 없는 사용자
이번 글은 길이가 좀 깁니다. 내용은 복잡한 것이 아니니 길다고 중간에 포기하지 마시고 쭉 읽어주세요^^


이전 회에서는 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일 글을 공개할 때 태스크 그룹의 스레드 세이프 특성을 잘 못 이해하여 잘못된 내용을 전달하였습니다. 그래서 오늘 글을 다시 수정하였습니다. ;;;;;;

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

Parallel Patterns Library (PPL)

VC++ 10 Concurrency Runtime 2009. 8. 6. 06:00 Posted by 알 수 없는 사용자

이제 본격적으로 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 예제를 설명한 글을 올린 적이 있으니 먼저 그것을 보면서 예습을 하는 것도 좋습니다.



Concurrency Runtime

VC++ 10 Concurrency Runtime 2009. 7. 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)에 대해서 이야기 하겠습니다.^^