SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

async、awaitそしてTaskについて(非同期とは何なのか)

業務ではC#をやっていないのですが、とある日、C#を使った業務をやっている同僚から、

asyncとかawaitとか、よく分からない。なんでメソッド名にAsync付けるんだよ。Taskってなに。

というような、呪詛のような言葉を聞きました。
その辺詳しいですか?と言われ、一通り説明したところ、納得してもらえたようだったので、自分なりの説明をブログにも書いてみます。

同期処理と非同期処理

一言で言います。

  • 同期処理
    処理が終わるまで待つ

  • 非同期処理
    処理が終わるまで待つかどうかを、利用者に委ねる

です。

処理が終わった後、特にやることが無ければ、非同期処理呼び出して放置、というのもアリです。*1

そして、asyncが付いたメソッドは、非同期メソッドとなります。

サンプル

.NET Core 1.1を利用しています。

  • 非同期メソッド
public async Task<string> GetResponseAsync(string url) {

    if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) {
        return "This URL is bad format!";
    }

    using (var client = new HttpClient()) {

        var response = await client.GetAsync(url, HttpCompletionOption.ResponseContentRead);

        return await response.Content.ReadAsStringAsync();
    }
}

GETリクエストの結果を返すだけのメソッドです。
asyncが付いているため、非同期メソッドとなります。

戻り値はTask<string>となっていますが、実際には単にstringを返しています。

asyncの効果

asyncが付いたメソッドは、単にこのメソッドは非同期だよ!ということを示します。*2

async (C# リファレンス) | Microsoft Docs

asyncを付けたメソッドには、通常のメソッドと異なり、いくつかの効果が生まれます。

IDE等が、非同期メソッドとして扱う

まず、これです。

下の画像のように、asyncが付いているメソッドは、待機可能と出ます。

f:id:hiroronn:20171005202436p:plain

言葉通り、「待つことができる」なので、待つかどうかは、利用者の選択となります。

戻り値の指定

  • 戻り値で、Taskを指定した場合、戻り値を返す必要は無い(返せない)
  • 戻り値で、Task<T>を指定した場合、returnでT型の値を返せばよい

上のソースでもそうですが、メソッドの戻り値でTask<string>と指定しているにもかかわらず、response.Content.ReadAsStringAsync()ではstring型を返します。
このように、Task<T>Tで指定した型を返せばよくなります。むしろ、Taskを返そうとするとエラーになります。
戻り値無し(void)の場合は、通常のvoidと同じく、戻り値を返す必要はありません。

awaitの使用

  • awaitがキーワード扱いされる
  • awaitが現れたら、その処理は非同期(別スレッド)で行われ、メソッドの処理が終わる
  • awaitで指定された処理が終わったら、その先の処理が流れ出す

awaitは、asyncキーワードによって変更された非同期メソッドでのみ使用できます。

と、Microsoft Docksにも記載があります。*3

await (C# リファレンス) | Microsoft Docs

まず、上記「メソッドの処理が終わる」の意味です。
サンプルソース中、await client.GetAsyncの処理に到達したら、このメソッドを抜けてしまいます。

そして、awaitで呼び出している処理(ここでは、Getリクエストを投げて、結果を受け取る)を、別スレッドで実行します。

その後、リクエストを実行し、結果が返ってきたら、var response = await client.GetAsyncresponseに値がセットされますが、このタイミングで続きが開始します。

とにかく、await以降はawaitで指定した処理が終わるまで実行されない、と覚えておきましょう。

async voidの無意味さ

さて、asyncを付けた場合について話しました。
では、async voidだとどうなるのか、についてです。

先ほど、非同期処理は、「待つかどうかを利用者に委ねる」と書きましたが、その「待つ」という仕組みを提供しているのが、Taskというクラスです。

「終了を待つ」には、以下のように書きます。

var task = accessor.GetResponseAsync("http://hatenablog.com/");
task.Wait();

//以下でもよい
string result = task.Result;

終了を待つ場合は、awaitを使わず、Task<T>をそのまま受け取ります。

さて、以下の非同期メソッドの終了を「待つ」場合、どうしましょう?

public async void PostAsync(string url, string value) {
    var client = new HttpClient();
    var content = new FormUrlEncodedContent(new[] {
        new KeyValuePair<string, string>("value", value)
    });

    var response = await client.PostAsync(url, content);

    if (!response.IsSuccessStatusCode) {
        throw new HttpRequestException("Request Failed!");
    }
}

こう定義された場合、上のように、var task = PostAsync(~);とすると、何が起こるでしょうか?



はい、分かった人は今度からちゃんとTaskを返してあげましょう。

正解は、「voidだと戻り値が無くて、呼び出し元が待つ手段が無くなる」です。*4
待つという選択肢を、呼び出し元にも渡してあげてください…

また、async voidでこうやって書いてしまうと、非同期処理が終わったかどうか、成功したかどうかを完全に無視する形となりますので、注意しましょう。

async void を使ってよい場合

UWP、WPF等、クライアントのイベントに反応する場合は、async voidを使ってよい、と言われています。

なぜ、という部分は、使い方をしっかり解説した記事作って、そちらで伝えたいです。文量とソースが必要でして…すみません。

結局、Taskってなに

Taskは、非同期処理の入れ物だと考えるようにしています。

入れ物の中がどうなっているのか(実行中か、キャンセルされたのか、終わるまで待つのか等)は、入れ物であるTaskが教えてくれます。

入れ物が、「処理終わったよー」と言ってきたら、必要な処理をすればいいのです。


結局、awaitで待つ、というのは、「処理終わったよー」を検知し、続きの処理をやってくれる、便利な仕組みです。それだけです。

サンプル補足

awaitで指定された処理は非同期になる、という話をしました。

では、awaitに至る前の、

if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) {
    return "This URL is bad format!";
}

の処理は、どう考えたらよいでしょうか?

これは、デバッグ実行したらわかると思います。ぜひ試してください。

おわりに

非常に単純な例ですが、asyncawaitのことが、ちょっとでもわかっていただけたらいいな、と思います。

特に、戻り値がTaskである意味が理解できたなら、一歩前進です。

非同期処理が簡単に書けるようになったとはいえ、非同期処理自体が難しいのです。私含め、使い方は徐々に勉強していく、という感じで頑張りましょう。

今回のソースは、Githubにアップロードしています。良ければご覧ください。

github.com


*1:それでいいのか、は置いておく

*2:示すだけで、内部的に処理が非同期で行われるかどうかは関係ない。ただし、非同期処理(await)が無いと、警告が出る

*3:訳が誤っているような。キーワードによって修飾とか?

*4:そもそもコンパイルに失敗する