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
が付いているメソッドは、待機可能と出ます。
言葉通り、「待つことができる」なので、待つかどうかは、利用者の選択となります。
戻り値の指定
- 戻り値で、
Task
を指定した場合、戻り値を返す必要は無い(返せない) - 戻り値で、
Task<T>
を指定した場合、returnでT
型の値を返せばよい
上のソースでもそうですが、メソッドの戻り値でTask<string>
と指定しているにもかかわらず、response.Content.ReadAsStringAsync()
ではstring
型を返します。
このように、Task<T>
のT
で指定した型を返せばよくなります。むしろ、Task
を返そうとするとエラーになります。
戻り値無し(void
)の場合は、通常のvoid
と同じく、戻り値を返す必要はありません。
awaitの使用
await
がキーワード扱いされるawait
が現れたら、その処理は非同期(別スレッド)で行われ、メソッドの処理が終わるawait
で指定された処理が終わったら、その先の処理が流れ出す
await
は、async
キーワードによって変更された非同期メソッドでのみ使用できます。
await (C# リファレンス) | Microsoft Docs
まず、上記「メソッドの処理が終わる」の意味です。
サンプルソース中、await client.GetAsync
の処理に到達したら、このメソッドを抜けてしまいます。
そして、await
で呼び出している処理(ここでは、Getリクエストを投げて、結果を受け取る)を、別スレッドで実行します。
その後、リクエストを実行し、結果が返ってきたら、var response = await client.GetAsync
のresponse
に値がセットされますが、このタイミングで続きが開始します。
とにかく、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!"; }
の処理は、どう考えたらよいでしょうか?
これは、デバッグ実行したらわかると思います。ぜひ試してください。
おわりに
非常に単純な例ですが、async
やawait
のことが、ちょっとでもわかっていただけたらいいな、と思います。
特に、戻り値がTask
である意味が理解できたなら、一歩前進です。
非同期処理が簡単に書けるようになったとはいえ、非同期処理自体が難しいのです。私含め、使い方は徐々に勉強していく、という感じで頑張りましょう。
今回のソースは、Githubにアップロードしています。良ければご覧ください。