かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Windows ストア アプリで重要な非同期プログラミング(C#)について復習

ここでは、C# 5.0で追加された非同期プログラミングのための構文について説明します。

非同期プログラミングの必要性

非同期プログラミングは、Windows ストア アプリでは重要な要素です。50ms以上かかる可能性のあるAPIのほとんどが非同期として提供されています。これは、時間のかかる処理を、同期的に呼び出した場合にアプリケーションの応答が、止まってしまうという問題を起こさないためです。時間のかかる処理を同期的に行った場合のイメージを以下に示します。


このような処理は、タブレットなどのタッチユーザーインターフェースではユーザーに非常にストレスになります。Windows ストア アプリでは避けるべき挙動になります。ここで、時間のかかる処理を非同期に行った場合のイメージを以下に示します。

このような処理を簡単に実装できるようにC#5.0から導入されたasync修飾子とawait演算子について説明します。

async修飾子・await演算子

Windows ストア アプリでの非同期APIを使ったコード例を以下に示します。

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    // 何か時間のかかる処理をバックグラウンドで行う
    var result = await Task.Run(() =>
    {
        // 時間のかかる処理
        return “result”;
    });

    // 結果の表示
    textBlock.Text = result;
}

このコードで使用しているasyncというメソッドの修飾子と、awaitという演算子について動作を説明します。async修飾子は、メソッド内でawait演算子を使用するということを明示するための修飾子です。この修飾子がないメソッドでは、await演算子を使った非同期処理は行えません。await演算子は、TaskクラスやIAsyncInfoインターフェースやIAsyncOperationWithProgressインターフェースなどの待機可能な値を受け取り、終了するまで待機します。
await演算子による待機の特徴は、現在実行中のスレッドはブロックせずにバックグラウンドの処理の終了を待機して、バックグラウンドの処理が終わった時点で、現在実行中のスレッドで続きの処理を開始する点です。このため、ユーザーインターフェースを扱うスレッドからバックグランドで行う処理を実行し、結果を受け取りユーザーインターフェースにフィードバックするといった処理が簡単に行えるようになります。

async修飾子・await演算子の記述例

async修飾子とawait演算子の記述例をいくつか示します。

戻り値がvoidのメソッド

戻り値がvoidのメソッドにasync修飾子がついているケースです。これは、呼び出し側で待つことが出来ないため、完全に突き放しの処理になります。基本的にイベントハンドラに対して使用する以外で使用することはありません。

async void Foo()
{
    // 何か時間のかかる処理
    await …();
}

呼び出し側のコード例を以下に示します。

// 呼び出すことは出来るが結果を待つことは出来ない
Foo();
戻り値がTaskのメソッド

戻り値がTaskのメソッドにasync修飾子がついているケースです。これは、呼び出し側で終了を待機することが出来ます。

async Task Foo()
{
// 何か時間のかかる処理
    await …();
}

呼び出し側のコード例を以下に示します。

// Fooの処理結果が終わるまで待機する
await Foo();
戻り値がTaskのメソッド

戻り値がTaskのメソッドにasync修飾子がついているケースです。これは、呼び出し側で終了を待機しつつ処理結果を受け取ることが出来ます。処理結果の型はT型になります。

async Task<string> Foo()
{
    // 時間のかかる処理
    await …();
    // returnはTask<T>を返すのではなくT型の値を返せば良い
    return “result”;
}

コードのコメント中にもありますが、async Taskのメソッドでは、メソッド内で返す値はTで指定した型の値を返すだけで自動的にTaskになるようにコンパイラが変換してくれます。呼び出し側のコード例を以下に示します。

// awaitで待つことでメソッドの結果を受け取れる
string value = await Foo();
// 受け取った値は特にTask<T>の戻り値ということを意識することなく使える
textBlock.Text = value;
ラムダ式にasync修飾子を付けるケース

async修飾子はラムダ式にもつけることができます。コード例を以下に示します。

// ラムダ式にasyncを付けることも可能
Func<Task<int>> action = async () =>
{
    await Task.Delay(1000);
    return 10;
};

// 呼び出し結果をawaitで待機可能
int result = await action();

処理中で使っているTask.Delayメソッドは、引数で渡した時間だけ待機するメソッドです。await演算子と組み合わせることで、スレッドをブロックすることなくスリープのような処理をおこなえます。

foreachでawait演算子

await演算子はTask型などの待機可能なものを返す式の前なら何処にでも記述可能です。そのため以下のような書き方も可能です。

// Task<IEnumerable<T>> を返すGetDataAsyncメソッドがあるとする
foreach (var item in await GetDataAsync())
{
    // itemの処理を行う
}

待機可能なメソッド

WinRTでは、待機可能なメソッドが大量に定義されています。そのメソッドは名前の末尾にAsyncがついています。そのため、メソッド名からawait可能か判断できます。ただ、すべてのメソッドが、その命名規約に従っているわけではないので、最終的には各メソッドのドキュメントかインテリセンスでに表示される”(待機可能)”という表示から判断してください。

Taskの合成

Taskクラスには、Taskを扱った非同期処理をまとめるメソッドが定義されています。これを使うことで複数の処理を平行して実行するといったことが可能になります。

WhenAllメソッド

引数で渡したTaskやTaskをすべて待機します。以下の3つのオーバーロードが定義されています。

public static Task<TResult[]> WhenAll<TResult>(IEnumerable<Task<TResult>> tasks);
public static Task WhenAll(IEnumerable<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks);

引数で渡したTaskが結果を返すものの場合、すべての結果をTaskという形で受け取ることが可能です。このメソッドを使うと、複数のサーバーからデータをダウンロードして全てのダウンロードが完了を待って処理を行うといったことが簡単に行えます。

var client1 = new HttpClient();
var client2 = new HttpClient();
var client3 = new HttpClient();

// 3か所すべてGETが終わるまで待つ
HttpResponseMessage[] results = await Task.WhenAll(
    client1.GetAsync("http://...."),
    client2.GetAsync("http://...."),
    client3.GetAsync("http://...."));

// 結果を受け取り処理を行う
WhenAnyメソッド

WhenAnyメソッドは、引数で渡したTaskやTaskの中から、どれかが完了するまで待機するメソッドです。メソッドには、結果を返すもの、返さないものを含めて以下の4つのオーバーロードが定義されています。

public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks);
public static Task<Task> WhenAny(IEnumerable<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(params Task<TResult>[] tasks);
public static Task<Task> WhenAny(params Task[] tasks);

このメソッドを使うことで、複数個所にアクセスして、一番早く応答を返した所の結果を使って続きの処理を行うといった処理が行えます。

var client1 = new HttpClient();
var client2 = new HttpClient();
var client3 = new HttpClient();

// どれかのGETが終わるまで待つ(WhenAllと異なりawaitの結果は配列ではなく単一の値)
Task<HttpResponseMessage> resultTask = await Task.WhenAny(
    client1.GetAsync("http://...."),
    client2.GetAsync("http://...."),
    client3.GetAsync("http://...."));

HttpResponseMessage result = await resultTask;
// 結果を受け取り処理を行う