「Intel Concurrent Collections for C++」(CnC)とは
実際並列処理はややこしいしおっかないです。大きなキッチンで大勢のコックが包丁とフライパン振り回しているようなもので、ほっとけばしっちゃかめっちゃかになっても不思議じゃない。各人はそれぞれの役割に専念し、他人のふるまいを気にせず勝手に働きながらも全体としては統制のとれた動きを作り出さにゃなりません。mutexで排他制御したりjoinで完了を待ったり、さまざまな道具を駆使して統制のとれた動きを作り出しているのはプログラマであり、そこがいちばん難しく悩ましいところです。
C++,C#あるいはVB,Javaもそうですが、コードは原則的に並べた順に実行されます。
int main() { f(); g(); h(); }
と書けば、まずf()、それが終わればg()、しかるのちh()の順で呼び出されます。たとえこの3つをどの順にやっても(あるいは同時にやっても)構わないとしても、です。
ここで魔法の箱を用意します。魔法の箱に3つの仕事を投げ込んで"開始"ボタンを押すと投げ込んだすべての仕事をこなし、全部終わったら"終了"ランプが点灯する、と。魔法の箱の中にはコビトさんが住んでいます。何人いるかは分からないけど、みんなで手分けして仕事をさばいてくれるので、たくさんいればそれだけ早く"終了"ランプが点灯します。並列処理って要するにそーゆーこと。
この魔法の箱に"材料を刻む"/"カレーを煮込む"/"米を研ぐ"/"飯を炊く"/"ご飯にカレーをかける"なんてな仕事を投げ込んで"開始"ボタンを押してしばらく待つとカレーライスが出て...きません。"カレーを煮込む"と"飯を炊く"は同時にできますが、"飯を炊く"は"米を研ぐ"が完了していなくてはなりません。仕事には依存関係がありますからね、コビトさんにはそれを教えておかないと。
そんなわけで、魔法の箱にはいくつかの仕事とそれらの依存関係とを投げ込みます。コビトさんたちは依存関係を守りつつ、手分けして仕事を片付けます。どのコビトさんに/どの仕事を/どの順序で割り振るかは魔法の箱が考えてくれる、そんな魔法の箱がCnCです。
インストール
CnCのページからZIPをダウンロード/解凍するとインストール・パッケージ:msiが出てきますから、そいつをつついてインストールします。CnCは同じくIntel製TBB(Threading Building Blocks)をベースに作られているのでTBBのインストールもお忘れなく(CnCとTBBの抱き合わせパッケージも用意されているみたいです)。インストールが完了したら環境変数CNCROOT,TBBROOTがセットされていることを確認してください。
魔法の箱に食わすもの
魔法の箱を構成するのは:
- step:コビトさんにやってもらう仕事
- item:stepを行うために必要な材料(入力)および仕事によって作られる結果(出力)
- tag:stepをやってもらうための注文伝票
この3つを魔法の箱にセットしてtagを投げ込めば、中のコビトさんが仕事をしてくれます。"終了"ランプが点くのを待ってitemを取り出せば美味しいカレーライスのできあがり。
ただし、stepには守らなければならない鉄の掟が2つあります。
[1] stateless
処理単位(step)はその実行において状態を持ってはならない。すなわち、いかなるグローバルデータ/スタティックデータにもアクセスしてはならない。処理単位に許されるデータ・アクセスはitemを読むこと(get)と、itemを書くこと(put)だけである。
[2] immutable
データを書き換えてはならない。一度書き込んだitemに再度putしてはならない。値を変更したいなら異なる(未使用の)itemをputすることで実現せよ。
この2つを守っていればデータの競合は発生せず、各stepの実行制御をCnCに委ねることができます。
マルチスレッドを意識しないマルチスレッド
いくつかサンプルをご覧に入れましょうか。まずは入出力のない単純な"Hello, world"から。
まずコビトさんにやってもらうお仕事(step)の宣言。
struct my_contexst; // 前方宣言 struct my_step { int execute(const int& n, my_context& ctx) const; };
stepのエントリはint execute(const <tag型>&, <コンテキスト>&)constとするのがお約束です。第一引数は注文伝票となるtag型、好きに選べるのでintとしました。第二引数はコンテキスト、これが魔法の箱に相当します。
では魔法の箱であるmy_context。
struct my_context : public CnC::context<my_context> { friend struct my_step; CnC::step_collection<my_step> steps; CnC::tag_collection<int> tags; mutex mtx; my_context() : steps(*this), tags(*this) { tags.prescribes(steps, *this); // tags は steps に 指示する } };
コンテキストはCnC::context<>から導出します。テンプレート引数にはコンテキストそれ自身を与えるという奇妙な形式なのに注意。
コンテキストのナカミはstepの集合CnC::step_collection<>とtagの集合CnC::tag_collection<>テンプレート引数の型はそれぞれ宣言したstep型とtag型、どちらもコンストラクタで初期化し、tagとstepとの関係を教えてあげます。
stepの処理を定義しましょう。"Hello, world"を出力し、少しの間眠ってもらいます。
int my_step::execute(const int& n, my_context& ctx) const{ { lock_guard<mutex> guard(ctx.mtx); cout << n << " : Hello, world" << endl; } // ちょっとおやすみ chrono::milliseconds duration(1000 - n*50); this_thread::sleep_for(duration); { lock_guard<mutex> guard(ctx.mtx); cout << n << " : Good bye." << endl; } return CnC::CNC_Success; }
これでお仕事と依存関係を詰め込んだ箱ができました。