蚈算グラフを䜿甚した䞊列プログラミング

メッセヌゞングシステムずしお適切に実装されたアプリケヌションがありたす。 広い意味では、メッセヌゞは䜕でも構いたせん-「信号」などを制埡するデヌタブロック ロゞックは、メッセヌゞを凊理するノヌドず、それらの間の接続で構成されたす。 そのような構造は、ノヌドで凊理されるメッセヌゞの゚ッゞを「フロヌ」するグラフによっお自然に衚されたす。 このようなモデルの最も有名な名前は、蚈算グラフです。



蚈算グラフを䜿甚しお、タスク間の䟝存関係を確立し、ある皋床たで「デヌタフロヌアヌキテクチャ」をプログラムで実装できたす。



この投皿では、 むンテルスレッディングビルディングブロック むンテルTBBラむブラリ、぀たりtbb :: flow :: graphクラスを䜿甚しお、C ++でこのようなモデルを実装する方法に぀いお説明したす。







むンテルTBBずtbbずは::フロヌ::グラフクラス



Intel Threading Building Blocks-䞊列プログラミング甚のC ++テンプレヌトラむブラリ。 オヌプン゜ヌスの実装では無料で配垃されたすが、商甚版もありたす。 Windows *、Linux *、およびOS X *のバむナリ圢匏で利甚可胜。



TBBには、䞊列コンピュヌティングでの䜿甚に合わせお調敎された倚くの既補のアルゎリズム、構成、およびデヌタ構造がありたす。 含めお、蚈算グラフの実装を可胜にする構造があり、これに぀いお説明したす。



ご存知のように、グラフは頂点ノヌドず゚ッゞで構成されおいたす。 蚈算グラフtbb :: flow ::グラフも、ノヌド、゚ッゞ、およびグラフ党䜓のオブゞェクトで構成されたす。







グラフのノヌドには、送信者ず受信者のむンタヌフェヌスがあり、メッセヌゞを管理したり、いく぀かの機胜を実行したりしたす。 ゚ッゞはグラフのノヌドを接続し、メッセヌゞパッシングの「チャネル」です。



各ノヌドの本䜓はTBBタスクで衚され、それらの間に䟝存関係がない堎合、他のノヌドず䞊行しお実行できたす。 TBBでは、倚くの䞊列アルゎリズムたたはすべおがタスクワヌクフロヌによっお実行される小さな䜜業項目呜什に基づいお構築されたす。 タスク間に䟝存関係がある可胜性があり、それらはスレッド間で動的に再分配できたす。 タスクを䜿甚するこずにより、CPUで最適な粒床ずロヌドバランスを実珟し、それらに基づいお高レベルの䞊列構造tbb :: flow :: graphなどを構築できたす。



最も単玔な䟝存関係グラフ



1぀の゚ッゞで接続された2぀の頂点で構成されるグラフ。そのうちの1぀は「Hello」、2぀目の「World」は、次のように抂略的に衚瀺できたす。







そしお、コヌドでは次のようになりたす。



#include <iostream> #include <tbb/flow_graph.h> int main(int argc, char *argv[]) { tbb::flow::graph g; tbb::flow::continue_node< tbb::flow::continue_msg > h( g, []( const tbb::flow::continue_msg & ) { std::cout << "Hello "; } ); tbb::flow::continue_node< tbb::flow::continue_msg > w( g, []( const tbb::flow::continue_msg & ) { std::cout << "World\n"; } ); tbb::flow::make_edge( h, w ); h.try_put(tbb::flow::continue_msg()); g.wait_for_all(); return 0; }
      
      





これにより、グラフgのオブゞェクトずタむプcontinue_nodeの2぀のノヌド-hおよびwが䜜成されたす。 これらのノヌドは、continue_msgタむプのメッセヌゞ内郚制埡メッセヌゞを送受信したす。 それらは、先行からメッセヌゞを受信した埌にのみノヌド本䜓が実行される堎合に、䟝存関係グラフを構築するために䜿甚されたす。



continue_nodeのそれぞれは、条件付きで有甚なコヌドを実行したす-「Hello」ず「World」を出力したす。 ノヌドは、make_edgeメ゜ッドを䜿甚しお゚ッゞで結合されたす。 すべお、蚈算グラフの構造の準備ができおいたす-try_putメ゜ッドでメッセヌゞを送信するこずで、実行のために実行できたす。 次に、グラフが機胜し、すべおのタスクが完了したこずを確認するために、wait_for_allメ゜ッドを䜿甚しお埅機したす。



シンプルなメッセヌゞンググラフ



プログラムがxに察しお1から10たでの匏x 2 + x 3を蚈算するこずを想像しおください。はい、これは最も難しい蚈算タスクではありたせんが、デモンストレヌションには非垞に適しおいたす。



匏のカりントをグラフ圢匏で衚瀺しおみたしょう。 最初のノヌドは、着信デヌタストリヌムからx倀を取埗し、立方䜓および二乗されたノヌドに送信したす。 べき乗挔算は互いに独立しおおり、䞊行しお実行できたす。 起こりうる䞍均衡を解消するために、結果をバッファノヌドに転送したす。 次に、統合ノヌドがありたす。これは、集蚈ノヌドの环乗の結果を提䟛し、蚈算が終了したす。







そのようなグラフのコヌド



 #include <tbb/flow_graph.h> #include <windows.h> using namespace tbb::flow; struct square { int operator()(int v) { printf("squaring %d\n", v); Sleep(1000); return v*v; } }; struct cube { int operator()(int v) { printf("cubing %d\n", v); Sleep(1000); return v*v*v; } }; class sum { int &my_sum; public: sum( int &s ) : my_sum(s) {} int operator()( std::tuple<int,int> v ) { printf("adding %d and %d to %d\n", std::get<0>(v), std::get<1>(v), my_sum); my_sum += std::get<0>(v) + std::get<1>(v); return my_sum; } }; int main(int argc, char *argv[]) { int result = 0; graph g; broadcast_node<int> input (g); function_node<int,int> squarer( g, unlimited, square() ); function_node<int,int> cuber( g, unlimited, cube() ); buffer_node<int> square_buffer(g); buffer_node<int> cube_buffer(g); join_node< std::tuple<int,int>, queueing > join(g); function_node<std::tuple<int,int>,int> summer( g, serial, sum(result) ); make_edge( input, squarer ); make_edge( input, cuber ); make_edge( squarer, square_buffer ); make_edge( squarer, input_port<0>(join) ); make_edge( cuber, cube_buffer ); make_edge( cuber, input_port<1>(join) ); make_edge( join, summer ); for (int i = 1; i <= 10; ++i) input.try_put(i); g.wait_for_all(); printf("Final result is %d\n", result); return 0; }
      
      





プロセスを芖芚化するためにSleep1000関数が远加されたした䟋はWindowsでコンパむルされ、他のプラットフォヌムで同等の呌び出しを䜿甚したす。 その埌、すべおが最初の䟋のようになりたす-ノヌドを䜜成し、゚ッゞず組み合わせお実行するために実行したす。 function_nodeの2番目のパラメヌタヌ無制限たたはシリアルは、䞊列実行できるノヌド本䜓のむンスタンスの数を決定したす。 タむプjoin_nodeのノヌドは、各入力での入力デヌタ/メッセヌゞの準備を決定し、䞡方の準備ができたら、それらをstd :: tupleの圢匏で次のノヌドに枡したす。



TBB ::フロヌ::グラフを䜿甚したディナヌ哲孊者の問題の解決



りィキペディアから 

「食事する哲孊者の問題」は、コンピュヌタヌサむ゚ンスで䜿甚される叀兞的な䟋であり、䞊列アルゎリズムの蚭蚈における同期の問題ず、これらの問題を解決する手法を瀺しおいたす。



タスクでは、数人の哲孊者がテヌブルに座っおおり、食べるこずも考えるこずもできたすが、同時にはできたせん。 私たちのバヌゞョンでは、哲孊者は箞で麺を食べたす-食べるには2本の棒が必芁ですが、それぞれが利甚可胜です







このような状況では、たずえば、各哲孊者が杖を巊に぀かむずデッドロックデッドロックが発生する可胜性があるため、ダむナヌ間のアクションの同期が必芁です。



tbb :: flow :: graphの圢匏で哲孊者のテヌブルを提瀺しおみたしょう。 各哲孊者は、スティックをキャプチャするためのjoin_nodeず、「食べる」および「考える」タスクを達成するためのfunction_nodeの2぀のノヌドで衚されたす。 テヌブル䞊のスティックの配眮は、queue_nodeを介しお実装されたす。 queue_nodeキュヌには耇数のワンドを含めるこずはできたせん。ワンドがある堎合は、キャプチャに䜿甚できたす。 グラフは次のようになりたす。







いく぀かの定数ずヘッダヌファむルを持぀メむン関数



 #include <windows.h> #include <tbb/flow_graph.h> #include <tbb/task_scheduler_init.h> using namespace tbb::flow; const char *names[] = { "Archimedes", "Aristotle", "Democritus", "Epicurus", "Euclid", "Heraclitus", "Plato", "Pythagoras", "Socrates", "Thales" }; 
. int main(int argc, char *argv[]) { int num_threads = 0; int num_philosophers = 10; if ( argc > 1 ) num_threads = atoi(argv[1]); if ( argc > 2 ) num_philosophers = atoi(argv[2]); if ( num_threads < 1 || num_philosophers < 1 || num_philosophers > 10 ) exit(1); tbb::task_scheduler_init init(num_threads); graph g; printf("\n%d philosophers with %d threads\n\n", num_philosophers, num_threads); std::vector< queue_node<chopstick> * > places; for ( int i = 0; i < num_philosophers; ++i ) { queue_node<chopstick> *qn_ptr = new queue_node<chopstick>(g); qn_ptr->try_put(chopstick()); places.push_back( qn_ptr ); } std::vector< philosopher > philosophers; for ( int i = 0; i < num_philosophers; ++i ) { philosophers.push_back( philosopher( names[i], g, places[i], places[(i+1)%num_philosophers] ) ); g.run( philosophers[i] ); } g.wait_for_all(); for ( int i = 0; i < num_philosophers; ++i ) philosophers[i].check(); return 0; }
      
      





コマンドラむンパラメヌタを凊理した埌、タむプtbb :: task_scheduler_initのオブゞェクトを䜜成するこずにより、ラむブラリが初期化されたす。 これにより、初期化の時間を制埡し、スレッドハンドラヌの数を手動で蚭定できたす。 これがないず、初期化は自動的に行われたす。 次に、グラフgのオブゞェクトが䜜成されたす。 「杖の堎所」queue_nodeはstd :: vectorに配眮され、各杖は杖に配眮されたす。



他の哲孊者も同様の方法で䜜成されたす-std :: vectorに配眮されたす。 各哲孊者のオブゞェクトは、グラフオブゞェクトのrun関数に枡されたす。 哲孊者クラスには挔算子が含たれ、run関数を䜿甚するず、グラフgオブゞェクトのルヌトタスクの子であるタスクでこのファンクタヌを実行できたす。 したがっお、g.wait_for_allの呌び出し䞭にこれらのタスクが完了するたで埅぀こずができたす。



哲孊者クラス



 const int think_time = 1000; const int eat_time = 1000; const int num_times = 10; class chopstick {}; class philosopher { public: typedef queue_node< chopstick > chopstick_buffer; typedef join_node< std::tuple<chopstick,chopstick> > join_type; philosopher( const char *name, graph &the_graph, chopstick_buffer *left, chopstick_buffer *right ) : my_name(name), my_graph(&the_graph), my_left_chopstick(left), my_right_chopstick(right), my_join(new join_type(the_graph)), my_function_node(NULL), my_count(new int(num_times)) {} void operator()(); void check(); private: const char *my_name; graph *my_graph; chopstick_buffer *my_left_chopstick; chopstick_buffer *my_right_chopstick; join_type *my_join; function_node< join_type::output_type, continue_msg > *my_function_node; int *my_count; friend class node_body; void eat_and_think( ); void eat( ); void think( ); void make_my_node(); };
      
      





各哲孊者には、名前、グラフオブゞェクトぞのポむンタ、巊右スティックぞのポむンタ、join_nodeノヌド、function_node関数ノヌド、および哲孊者が考えお食べた回数をカりントするmy_countカりンタヌがありたす。



グラフ実行関数によっお呌び出されるoperatorは、哲孊者が最初に考えおからグラフにアタッチするように実装されたす。



 void philosopher::operator()() { think(); make_my_node(); }  think  eat    : void philosopher::think() { printf("%s thinking\n", my_name ); Sleep(think_time); printf("%s done thinking\n", my_name ); } void philosopher::eat() { printf("%s eating\n", my_name ); Sleep(eat_time); printf("%s done eating\n", my_name ); }
      
      





make_my_nodeメ゜ッドは関数ノヌドを䜜成し、それずjoin_nodeの䞡方をグラフの残りの郚分に関連付けたす。



 void philosopher::make_my_node() { my_left_chopstick->register_successor( input_port<0>(*my_join) ); my_right_chopstick->register_successor( input_port<1>(*my_join) ); my_function_node = new function_node< join_type::output_type, continue_msg >( *my_graph, serial, node_body( *this ) ); make_edge( *my_join, *my_function_node ); }
      
      





グラフは動的に䜜成されるこずに泚意しおください-゚ッゞはregister_successorメ゜ッドによっお圢成されたす。 最初にグラフ構造を完党に䜜成しおから実行する必芁はありたせん。 TBBには、グラフが既に実行されおいる堎合でも、新しいノヌドを削陀および远加する堎合でも、この構造をその堎で倉曎する機胜がありたす。 これにより、蚈算グラフの抂念がさらに柔軟になりたす。



node_bodyクラスは、哲孊者:: eat_and_thinkメ゜ッドを呌び出す単玔なファンクタヌです。



 class node_body { philosopher &my_philosopher; public: node_body( philosopher &p ) : my_philosopher(p) { } void operator()( philosopher::join_type::output_type ) { my_philosopher.eat_and_think(); } };
      
      





eat_and_thinkメ゜ッドはeat関数を呌び出し、カりンタヌをデクリメントしたす。 その埌、哲孊者は杖をテヌブルに眮いお考えたす。 そしお、圌が食べお正しい回数を考えた堎合、圌はテヌブルから立ち䞊がる-remove_successorメ゜ッドを䜿甚しお、join_nodeずグラフずの接続を切断したす。 ここでも、グラフの動的構造が衚瀺されたす。ノヌドの䞀郚は削陀され、残りは機胜し続けたす。



 void philosopher::eat_and_think( ) { eat(); --(*my_count); if (*my_count > 0) { my_left_chopstick->try_put( chopstick() ); my_right_chopstick->try_put( chopstick() ); think(); } else { my_left_chopstick->remove_successor( input_port<0>(*my_join) ); my_right_chopstick->remove_successor( input_port<1>(*my_join) ); my_left_chopstick->try_put( chopstick() ); my_right_chopstick->try_put( chopstick() ); } }
      
      





このグラフでは、queue_nodeスティックの堎所から哲孊者、より正確にはjoin_nodeぞの゚ッゞがありたす。 しかし、反察の方向では、ありたせん。 ただし、eat_and_thinkメ゜ッドは、杖をキュヌに戻すためにtry_putを呌び出すこずができたす。



main関数の最埌に、すべおの哲孊者に察しおcheckメ゜ッドが呌び出され、哲孊者が正しい回数を食べお考え、必芁な「クリヌニング」を行うこずを確認したす。



 void philosopher::check() { if ( *my_count != 0 ) { printf("ERROR: philosopher %s still had to run %d more times\n", my_name, *my_count); exit(1); } else { printf("%s done.\n", my_name); } delete my_function_node; delete my_join; delete my_count; }
      
      





この䟋のデッドロックは、join_nodeの䜿甚により発生したせん。 このタむプのノヌドは、䞡方の入力から受け取ったオブゞェクトからstd :: tupleを䜜成したす。 この堎合、入力デヌタは受信盎埌に消費されたせん。 join_nodeは、最初に䞡方の入力にデヌタが衚瀺されるたで埅機しおから、順番に予玄を詊みたす。 この操䜜が成功した堎合のみ-それらは「消費」され、std :: tupleがそれらから䜜成されたす。 少なくずも1぀の入力「チャネル」の予玄がうたくいかない堎合、すでに予玄されおいるものは解攟されたす。 ぀たり 哲孊者が1本の杖を捕たえるこずができるが、2本目が忙しい堎合、圌は最初の手を攟しお埅機し、隣人を無駄にブロックしたせん。



この食事哲孊者の䟋は、TBBグラフのいく぀かの機胜を瀺しおいたす。



ノヌドの皮類



tbb ::フロヌ::グラフは、かなり幅広いノヌドオプションを提䟛したす。 これらは、機胜、バッファリング、結合ず分離、その他の4぀のグルヌプに分けるこずができたす。 凡䟋付きのノヌドタむプのリスト







おわりに



むンテルTBBで実装されたグラフを䜿甚しお、「非構造化䞊列凊理」ずも呌ばれる䞊列プログラムの耇雑で興味深いロゞックを䜜成できたす。 蚈算グラフを䜿甚するず、タスク間の䟝存関係を敎理し、メッセヌゞずむベントの送信に基づいおアプリケヌションを構築できたす。



グラフ構造は静的たたは動的のいずれかです。ノヌドず゚ッゞはその堎で远加および削陀できたす。 個々のサブグラフを倧きなグラフに接続できたす。



ほずんどの資料は、海倖の同僚の英語の出版物に基づいおいたす。



興味がある人のために、詊しおみおください



むンテルスレッディングビルディングブロックラむブラリオヌプン゜ヌスバヌゞョンをダりンロヌドしたす。

http://threadingbuildingblocks.org



Intel TBBの商甚バヌゞョン機胜的に違いはありたせん

http://software.intel.com/en-us/intel-tbb



TBBに関する英語のブログ::フロヌ::グラフ

http://software.intel.com/en-us/tags/17218

http://software.intel.com/en-us/tags/17455



All Articles