非同期I/O待ち

10 12月

今日までのブログでもすでに何回かお話していますが、ネットワークなど、待ち時間の多い処理は非同期に書かないといけません。

今日はその非同期の話をしましょう。

サンプル コード: http://code.msdn.microsoft.com/IO-c6443b55

非同期処理の例

昨日に引き続き、Azure Tableを例にとりましょう。

Azure Tableに対して、複数指定したPartition Keyでデータを検索したい場合があります。

昨日は「ユーザーの所持品」というような、ゲームっぽいデータを例にしました。この所持品みたいなものの場合、ユーザーIDをPartition Keyにしたりします。指定したユーザーの所持品を取得するような場合が、上記のようなデータ検索の例となります。

このような場合、たとえば検索したいキーを keys という引数で渡したとして、Containsを使いたいところなんですが、Azure TableのPartition Key相手にContainsは使えないんですよね。

var q = context.CreateQuery<SampleEntity>(tableName);
var items = q.Where(x => keys.Contains(x.PartitionKey));
foreach (var item in items) //
実行時エラー: Cotains はサポートされていません
{
    Console.WriteLine(item);
}

なので、仕方がなくキーの数だけループすることになるわけですが…

var q = context.CreateQuery<SampleEntity>(tableName);
foreach (var key in keys)
{
    var items = q.Where(x => x.PartitionKey == key);
    foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

これだと、キーごとに1件1件、Azure Tableの通信待ちをすることになります。

同期版

以下、説明を簡単化するために少々コードを変更しましょう。

foreach (var q in Common.GetQueries(context, keys))
{
    var ret = q.Execute(); //
同期実行しちゃだめー
    Common.Output(ret, w);
}

GetQueriesは指定した複数のキーから、複数CreateQueryするメソッドです。また、出力部分も、Outputメソッドに任せます。このコード中のwは、出力先のStreamWriterです。

これは、まあ、良くない例です。このコードはいわゆる同期実行(通信のレスポンスが帰ってくるまでスレッドをブロックする)です。絵で表すと、以下のようになります。

大部分が待ち時間(黒い部分)です。

赤い部分や青い部分は、1ミリ秒もかからないでしょうね、このくらいの処理だと。それに対して、レスポンス待ちは50~100ミリ秒くらいかかると思います。もちろん、環境(どこのデータ センター相手かとか)によりますが、私のパソコンからアジア データ センターまでは70ミリ秒くらいかかっていました。

1個あたり70ミリ秒です。キーが1万件あろうものなら、15分くらいかかると思います。1度に1万件の検索をすることはないにしろ、たとえばウェブ サーバーなら、数千人のユーザーが5・6件ずつ検索してくるというようなことも考えられるので、1万というのはそうおかしな数字ではないです。

マルチ スレッド化

もう1つ、まだあまり良くない例を挙げて、どこが良くないかについて説明しましょう。

Parallel.ForEach(Common.GetQueries(context, keys), q =>
{
    var ret = q.Execute(); //
別スレッドを立てて、その中で同期実行
    lock (w) Common.Output(ret, w);
});

Parallelクラスを使って、並列化しました。この場合、大まかには、以下のような処理の流れになります。

Parallelクラスの内部では、スレッド プールというもの使っています。スレッド プールは、平常時でも、CPUのコア数程度のスレッドを起動していて、まずはその数だけ処理を開始します。

さて、やっぱり、大半が待ち時間で、これらのスレッドは休眠状態に入ります。CPUは暇なわけで、そのままだと勿体ないので、新しいスレッドを立てはじめます。

そして最終的には以下のように、大量のスレッドで大量にレスポンス待ちしだします。

これだけで、待ち時間の無駄はだいぶ減ります。しかし、Parallelクラスは本来、こういう用途に使うものではありません。Parallelクラスは、CPUをフル稼働させるような処理を並列化するためのもので、CPUのコア数程度のスレッド数の時に最大の性能が出るように作られています。

上記の例のように、I/O待ち(特に通信のレスポンス待ち)にParallelクラスを使おうとすると、大量のスレッドが作られます(これも環境次第ですが、私のところでは70個くらいのスレッドが立ちました)。

スレッドは立てれば立てるだけコストになります。スレッドを立てた瞬間の処理も重たければ、スレッド1つ1つが使っているメモリも馬鹿にならず、スレッド間の切り替えにもオーバーヘッドがかかります。

非同期処理

さて、それではようやく良い例を。

この手の、I/O待ち時間が長い処理には、非同期実行するためのメソッドが別途用意されています。この非同期実行版を使うべきです。

上記の例の場合、同期版のExecuteメソッドの代わりに、非同期版のBeginExecuteメソッドを使います。ただ、このままでは少し使いにくいので(あと、C# 5.0を見据えて)、以下のような拡張メソッドを用意しておきます。非同期処理をTaskクラス化するものです。

public static Task<IEnumerable<T>> ExecuteAsync<T>(this DataServiceQuery<T> q)
{
    return Task.Factory.FromAsync<IEnumerable<T>>(q.BeginExecute, q.EndExecute, null);
}

そして、以下のように書きます。

var tasks = Common.GetQueries(context, keys)
    .Select(x => x.ExecuteAsync()
        .ContinueWith(t => { lock (w) Common.Output(t.Result, w); })
    ).ToArray(); // ToArray
を付けて、ここで全タスクを先に起動してしまう。
Task.WaitAll(tasks.ToArray());

こう書いた場合、処理の流れは以下のようになります。

待つだけのための無駄なスレッドはなくなります。I/O待ちキューは、「I/Oを待っている処理がある」という程度の小さい情報しか持ちません。スレッドを立てるのと比べると桁違いに低コストです。

CPUに余裕があるうちは、前節のParallelクラスを使ったものでもそれなりの実行時間になります。しかし、それではCPUを限界まで使えないということです。サーバー用途では、無駄遣いがすぐにハードウェア コストに直結しますし、非同期処理は非常に重要です。

広告

コメント / トラックバック1件 to “非同期I/O待ち”

Trackbacks/Pingbacks

  1. 新機能が入るまで « C#たんっ! - 2011/12/11

    […] 昨日のブログで説明した通り、I/O待ちをするような処理は、非同期版のメソッドを使って、スレッドを作らずに待つことが重要です。 […]

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。