モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

C++ (fork) Advent Calendar 2013 2 日目 ~並列処理~

 Advent Calendar 初参加です。いろいろとネタに関しては持ち合わせていますがどれだけ記事にできるかはわかりませんが、いろいろなネタを書いていきたいと思います。(さすがにカレンダー作成者なので 4 本ぐらいは書いたほうがいいかな、とは思っておりますw)

 ちなみにこちらのカレンダーです。http://www.adventar.org/calendars/211

 私自身、Visual C++ ばかり使っているので、Visual C++ ネタに偏ってしまうと思います。それでは、本題に入っていきます。

概要

  1. Windows における非同期プログラミング
  2. Visual C++ ではどのように非同期プログラミングが行われるか
  3. Concurrency Runtime を用いた並列処理
  4. Linux では…
  5. まとめ

1. Windows における非同期プログラミング

 .NET では、C# 5.0 や VB 11.0 で async/await パターンが導入され、非同期プログラミングにおける簡素な表記が用いることができるようになりました。もちろん C++ (これは Windows Runtime も含みます) でも追加されていきます。

1.1. そもそも async/await パターンが“今”、なぜ登場したのか

 Windows は昔から内部実装は非同期を採用していると聞きます (詳細は知りません)。ただ、ユーザーに見える形「API」では同期的な API として公開されているようで、結果として User Interface のスレッドが止まってしまう現象が起こってしまうわけです。

 ここの「User Interface のスレッドが止まってしまう現象」が重要であります。つまり開発者は「UI のスレッドを止まらないようにするのに何らかのプログラミングモデルの意識が必要」ということになります。

 たとえば、イベントパターンがあげられます。.NET ではよく採用される形ではないのでしょうか? とある関数を呼び出し、バックグラウンドスレッドでネットワーク接続処理を起動。取得が完了したら、イベントで受信したデータを渡す、というような形です。

 しかし、“Windows Phone” というプラットフォームが出るときに、開発班は見直しをしたのでしょう。非同期が積極的に採用されるようになったのはこのころが始まりだと記憶しています (私はこのころは非同期なんてめんどくせーよw とかいう非常に知識もなかった時期でした)。 原則、50 ms 以上かかる処理は非同期 API として公開、ということになりました。

 こういった経緯で Windows Runtime や WPF にも非同期が導入されましたが、従来の非同期は可読性が非常に悪いものでした。以下は C# における非同期プログラミング初期における記述です。

var req = WebRequest.CreateHttp( "http://mntone.net/" );
req.BeginGetResponse( ar =>
{
  var res = wq.EndGetResponse( ar );
  var value = new StreamReader( res.GetResponseStream() ).ReadToEnd();
  Dispatcher.BeginInvoke( () => MessageBox.Show( value ) );
}, null );

 これが従来の非同期というものでした。最後の Dispatcher.BeginInvoke は UI スレッドで MessageBox を Show するために Invoke を Begin する命令です。

 それでは Visual C++ における非同期プログラミングを見ていきましょう。

2. Visual C++ ではどのように非同期プログラミングが行われるか

 Concurrency Runtime という task クラスが存在します*1。まずは簡単なサンプルを見ていきましょう。

task<std::string> get_response()
{
  return create_task( □ { return 4; } )
    .then( □( task<int32_t> prev_task ) { return std::string( "res: " + prev_task.get() ) } );
}

 このように create_task から .then によってどんどんつなげていくことができます。たとえば、Web からデータを取得し取得できるまで裏で回され、次にそのデータを処理する段階があり、そしてその加工したデータを返す、というような一連操作も簡単に記述することができます。

 ちなみに、前の task で例外が飛んでくる可能性がある場合は適切に例外をとらえるべきとは言えます (通常最後の then に例外が飛んできますが、もちろん途中の then でも例外はとらえられます)。

 task は or や and をとることができ、2 つのリクエストを同時に発行し、両方が終了するまで待つ、のような高度なことも実装することができます。

3. Concurrency Runtime を用いた並列処理

 この Concurrency Runtime には並列処理のための記述*2もサポートされており、容易にマルチスレッド化することができます。

 まずは 3 つのタスクを生成し、すべてのタスクが完了するのを待ち、それを結果として表示するサンプルを提示します。

std::vector<task<const char*>> task_list;
task_list.emplace_back( create_task( □ { return "abc"; } ) );
task_list.emplace_back( create_task( □ { return "pqrs"; } ) );
task_list.emplace_back( create_task( □ { return "xyz"; } ) );

auto task = when_all( std::begin( task_list ), std::end( task_list ) );
std::vector<const char*> res = task.get();
for( const char* str : res )
{
  std::cout << str << std::endl;
}

 それではこれを並列実行してみましょう。

std::vector<task<const char*>> task_list;
task_list.emplace_back( create_task( □ { return "abc"; } ) );
task_list.emplace_back( create_task( □ { return "pqrs"; } ) );
task_list.emplace_back( create_task( □ { return "xyz"; } ) );

parallel_for_each( std::begin( task_list ), std::end( task_list ), □( task<const char*> str )
{
  std::cout << str.get() << std::endl;
} );

 並列実行されるとむちゃくちゃな順番で表示されます。スレッド管理は Concurrency Runtime によって管理されているので、楽ちんです。

 乱数で並列的に合計を求め、最終的に全体数で割って平均値を求めるプログラムを書いてみましょう。

const size_t c = 10000000u;
int32_t sum;
std::vector<int32_t> values( c );
std::generate( std::begin( values ), std::end( values ), std::mt19937( 42 ) );
parallel_for_each( std::begin( values ), std::end( values ), [&]( int32_t no )
{
  sum += no;
} );

int32_t ave = sum / c;
std::cout << ave << std::endl;

 えらい簡単ですよね? 並列化とか楽すぎますしね。

 ちなみに、タイム計測用のやつを使って 5 回の平均 (1 回目は捨て、2~6 回目の平均値をとる) を計算してみたところ、Core i7-2620M on Windows 8.1 Pro では 11131 ms つまり、1.1 秒ぐらいになりました。

 この例だと単一スレッドだと 4000 ms ぐらいで終わったので、実際無意味な並列化ではあります。同一変数にアクセスしているので、そのあたりが遅くなっている理由もあげられるでしょう。このあたりは並列化の問題なので特に重要視しません (今回は並列処理の書き方としてこんなのがあるよ! ってことなので。Concurrency Runtime 万歳)。

4. Linux では…

 C++ RESK SDK*3 という http_client や http_listener を実装したものがあります。この SDK は今現状では Windows と Linux しかサポートしていませんが、将来的には OS X/iOS なども視野に入れて動いているみたいです。http_client のポータブルな形として使うことができるんですが、LinuxOS X/iOS では Concurrency Runtime があるわけではありません。そこで、ポータブルにできるように ppl のポータブル版があります。

 あくまでもサブセットの位置づけなので並列処理はできませんが、もし並列処理系の声がデカくなれば、使えるようになるかもしれません… (OpenMP があるからぶっちゃけなくてもいいと思いますけどねw)

5. まとめ

 非同期処理からの並列処理ということで、Concurrency Runtime は非同期のためだけじゃなくて、並列処理もできるんだよ、ということ。STL との親和性が高いため、相互運用的な意味では非常に使いやすいということです。

 私は前からこの機能があると知っていましたが実際に計測してみたりは初めてで、若干戸惑う部分もありましたが、基本的には Microsoft の C++ 好きが組み立てている Runtime なので、STL の基本的な流れは変わっていないと思います。

 これらのプログラムはデスクトップでも動きますし、何もストア アプリのために作られた Runtime でないので、一度触れてみる価値は十分あると思います。興味を持ったら是非、MSDN を参照しながら並列処理プログラミングをエンジョイしていただければいいかと思います。

 おわり。


注釈
*1 task Class - MSDN http://msdn.microsoft.com/en-us/library/vstudio/hh750113.aspx
*2 Parallel Algorithms - MSDN http://msdn.microsoft.com/en-us/library/vstudio/dd470426.aspx
*3 C++ REST SDK - codeplex https://casablanca.codeplex.com/