xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

非同期メソッド入門 (4) - UIスレッドとの同期

非同期メソッド入門 (2) - async修飾子とawait演算子でも簡単に書きましたが、await以降の処理はコンパイラによって継続渡しという形に変換されます。そのときのサンプルを再掲します。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    await HeavyWork();    //--- 何か重たい処理
    this.button.IsEnabled = true;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    HeavyWork().ContinueWith(_ =>    //--- UIに同期的な継続処理
    {
        this.button.IsEnabled = true;
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

正しくは下のように変換/展開されているわけではないのですが、意味的にはこうなっています。変換後のコードを見ると、ContinueWithメソッドの第2引数に同期コンテキスト上での動作指示をするスケジューラーを設定していることが分かります。このような機能のおかげで、継続処理をUIスレッドに同期させて実行させることができます。

.NET Frameworkでは「UIコンポーネントの操作はUIスレッド上で行わなければならない」という決まりがあります。非同期メソッド入門 (1) - 非同期処理の歴史でも触れたように、これまでの非同期処理ではUIスレッド上に処理を戻すためにDispatcher.BeginInvokeControl.Invokeなどを利用しなければならず、非常に煩雑でした。今回、非同期メソッドが継続処理を自動的にUIスレッドに戻してくれるようになったおかげで、これらの煩雑さを排除し、スマートに記述することができるようになりました。

UIスレッドに戻すかどうかを明示的に制御

とは言え、場合によっては「継続処理をUIスレッドに戻さずにそのまま実行したい!」といったケースもあるかと思います。非同期メソッドでそのようなことができないのかというともちろんそのようなことはなく、同期するかどうかを制御する方法が提供されています

private async void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    await Task.Run(() => Thread.Sleep(3000)).ConfigureAwait(false);
    this.button.IsEnabled = true;  //--- UIスレッド上でないので例外が発生する
}

上に示すサンプルのようにConfigureAwaitメソッドを利用します。第1引数にはUIスレッドに同期させるかどうかのbool値を指定します。なので、上記のサンプルではUIスレッドに同期しないように指示していることになります。そしてこの場合、ボタンの有効化の処理はUIスレッド上で動作しないため例外が発生します。