前回の記事では、こんなコードを見ました。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await LoadDataAsync();
MessageText.Text = "完了しました";
}
LoadDataAsync() の処理を待っている間、UIスレッドはずっとそこで待ち続けているわけではありません。
それなのに、await が終わった後では普通にUIを更新できます。
前回は、
awaitの後の処理は、UIスレッド側で実行される
というところまで見てきました。
では、こんなコードにしたらどうでしょう。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await LoadDataAsync().ConfigureAwait(false);
MessageText.Text = "完了しました";
}
追加したのは、これだけです。
.ConfigureAwait(false)
ところが、この場合。
await の後の処理が、元のUIコンテキストで実行されることは保証されなくなります。
そのため、WPFのUIをそのまま操作しようとすると問題になる可能性があります。
……あれ?
awaitしたらUIスレッドに戻るんじゃなかったの?
今回は、この疑問を考えてみます。
そもそも「UIスレッドに戻る」ってどういうこと?
前回の記事では分かりやすく、
awaitの後はUIスレッドに戻る
と説明しました。
ただ、もう少し正確に見ると、
awaitそのものが「UIスレッドへ戻す」という単純な仕組みではありません。
WPFなどのUIアプリケーションでは、await する時点の実行環境を利用して、
非同期処理が終わった後の続きを、元のUI側で実行する
という動きができます。
ここで関係してくるものの一つが、
SynchronizationContext
です。
WPFではUIスレッドに対応した SynchronizationContext があり、通常の await では、こうした現在のコンテキストが継続処理に利用されます。
かなり単純化すると、こんなイメージです。
UIスレッド
↓
await SomeMethodAsync();
↓
処理が完了していなければ
いったん呼び出し元へ制御を返す
↓
非同期処理が完了
↓
捕捉していたコンテキストを利用して
await後の続きを実行
↓
MessageText.Text = "完了";
そのため、
await LoadDataAsync();
MessageText.Text = "完了";
のようなコードでも、通常はそのままUIを更新できます。
では、ConfigureAwait(false)は何をしているの?
ここで登場するのが、
ConfigureAwait(false)
です。
ざっくり言うと、
awaitが終わった後、元のコンテキストに戻ることを要求しない
という指定です。
例えば、
await LoadDataAsync().ConfigureAwait(false);
とすると、
UIスレッド
↓
await
↓
非同期処理が完了
↓
元のコンテキストへの復帰を要求しない
↓
await後の続きを実行
となります。
ここで大事なのは、
「別スレッドで実行する」と指定しているわけではない
ということです。
ConfigureAwait(false) は、
別スレッドで続きを実行してください
という命令ではありません。
あくまで、
元のコンテキストに戻る必要はありません
という指定です。
結果として、await の後の処理がUIスレッド以外で実行されることはあります。
しかし、
ConfigureAwait(false)を付けたら必ず別スレッドになる
という意味ではありません。
だからUIをそのまま操作できないことがある
例えば、
await LoadDataAsync().ConfigureAwait(false);
MessageText.Text = "完了";
と書いたとします。
通常の await とは違い、ConfigureAwait(false) を指定すると、元のUIコンテキストへの復帰を要求しません。
そのため、await の後のコードがUIスレッド上で実行されているとは限りません。
WPFのUI要素は、基本的にそれを所有するUIスレッドから操作する必要があります。
だから、
MessageText.Text = "完了";
のようなUI操作をそのまま行うと、例外になる可能性があります。
ここで重要なのは、
ConfigureAwait(false)がUIを操作できなくしているわけではない
ということです。
元のUIコンテキストへの復帰を要求しなくなった結果、
UIを操作するために必要な実行環境ではない可能性がある。
ということです。
falseって何がfalseなの?
僕は最初、この名前が分かりにくかったです。
ConfigureAwait(false)
何がfalseなの?
と思いませんか?
ざっくり言えば、
元のコンテキストを使って続きを実行しようとするか
についての false です。
通常の、
await SomeMethodAsync();
では、利用可能な現在のコンテキストを捕捉し、そこで続きを実行しようとします。
一方、
await SomeMethodAsync().ConfigureAwait(false);
では、
元のコンテキストに戻ることを要求しない
となります。
なので、
false = 非同期処理にする
ではありません。
そして、
false = 別スレッドで実行する
でもありません。
ここは混同しやすいところだと思います。
ConfigureAwait(false)を付ければ速くなる?
ここでまた、やりたくなることがあります。
await SomeMethodAsync().ConfigureAwait(false);
元のコンテキストに戻らなくてもいい。
だったら、
ConfigureAwait(false)を付けた方が速いんじゃない?
……と。
でも、
ConfigureAwait(false)を付ければ処理そのものが速くなる
という話ではありません。
SomeMethodAsync() が10秒かかる処理なら、
await SomeMethodAsync();
を、
await SomeMethodAsync().ConfigureAwait(false);
に変えたからといって、その処理が5秒になるわけではありません。
変わるのは、
await後の継続処理を、元のコンテキストで実行することを要求するかどうか
です。
もちろん、不要なコンテキスト復帰を避けることが性能面で意味を持つ場合はあります。
でも、
「ConfigureAwait(false)を付ければ処理が速くなる」
と覚えるのは違います。
ここでも、
「処理そのものにかかる時間」と「await後をどこで実行するか」は別の話
として考えた方が分かりやすいと思います。
では、なぜConfigureAwait(false)を使うの?
例えば、UIを一切操作しない処理だったらどうでしょう。
private async Task<string> LoadDataAsync()
{
var data = await GetDataAsync().ConfigureAwait(false);
return ConvertData(data);
}
このメソッドの中では、
- ボタンを操作する
- TextBlockを書き換える
- 画面を更新する
といったことをしていません。
つまり、
await後の処理を、元のUIコンテキストで実行しなければならない理由がありません。
こうしたUIに依存しないコードでは、ConfigureAwait(false) が使われることがあります。
特に、さまざまな環境から呼ばれるライブラリコードでは、
呼び出し元のコンテキストへ戻る必要がない
という設計になることがあります。
逆に、WPFの画面側で、
await SomeMethodAsync();
MessageText.Text = "完了";
と書きたいのであれば、元のUIコンテキストで継続できる通常の await の方が自然です。
つまり、
ConfigureAwait(false)は、とりあえず付けておけばいいおまじないではありません。
その後の処理が、
元のコンテキストで実行される必要があるのか?
を考えて使うものです。
「UIスレッドに戻る」と覚えるだけでは足りなかった
前回、
await LoadDataAsync();
MessageText.Text = "完了";
がなぜ動くのかを考えました。
そこで、
awaitしたらUIスレッドに戻る
だけで覚えてしまうと、
ConfigureAwait(false)
が出てきた瞬間、
「え?戻るんじゃなかったの?」
となります。
実際には、
await時のコンテキストを利用して、await後の続きをそこで実行する仕組みがある
と考えた方が分かりやすい。
そして、
ConfigureAwait(false)
は、
そのコンテキストへの復帰を要求しない
という指定です。
ここまで分かると、
await SomeMethodAsync();
と、
await SomeMethodAsync().ConfigureAwait(false);
の違いも見えてきます。
補足:ConfigureAwait(false)はいつでも必要?
ここは少しだけ注意が必要です。
ConfigureAwait(false) の使われ方は、
- WPFなどのUIアプリケーション
- ASP.NET
- ライブラリ
- .NET Framework
- 現在の.NET
など、実行環境やコードの役割によって考え方が変わります。
例えば、現在のASP.NET Coreには、従来のASP.NETと同じような SynchronizationContext はありません。
また、ライブラリコードでの ConfigureAwait(false) の扱いについても、対象とする.NETやライブラリの設計方針によって判断が変わります。
なので今回は、
「ConfigureAwait(false)は付けるべきか?」
ではなく、
「ConfigureAwait(false)を付けると、awaitの何が変わるのか?」
に絞っています。
まず仕組みを理解して、その上で必要な場所で使う。
その順番でいいと思います。
まとめ
今回のポイントです。
-
awaitそのものが単純にUIスレッドへ戻しているわけではない - WPFなどでは現在のコンテキストを利用して、
await後の続きをUI側で実行できる -
ConfigureAwait(false)は、元のコンテキストへの復帰を要求しない -
ConfigureAwait(false)は「別スレッドで実行する」という指定ではない - そのため、付けたからといって必ず別スレッドになるわけではない
-
ConfigureAwait(false)を付けても、処理そのものが速くなるわけではない - 大切なのは、
await後の処理が元のコンテキストを必要としているかどうか
非同期処理を勉強していると、
async
await
Task.Run
ConfigureAwait(false)
と、次々に新しいものが出てきます。
そして、それぞれを、
「画面を固めないための何か」
くらいで覚えてしまうと、だんだん分からなくなってきます。
僕自身、
ConfigureAwait(false)
を見たとき、
「falseにすると、何がどうなるの?」
となりました。
でも、
「awaitの後を、どこで実行する必要があるんだろう?」
と考えると、少し整理しやすくなります。
await の前だけではなく、
awaitした後に何をするのか。
そこまで見ると、ConfigureAwait(false) が何のためにあるのかも少し見えてくると思います。
関連記事
非同期処理について、こんな記事も書いています。