xin9le.net

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

非同期メソッド入門 (15) - 機械的読み替えに注意

async/awaitの挙動は非同期ですが、見た目はほぼほぼ同期的なコードと同じです。コードの可読性がこれまでとは桁違いに良くなるので、例えば既存プロジェクトを.NET Framework 4から.NET Framework 4.5に切り替えた場合などに既存のコードをasync/awaitで置き換えるかもしれません。動いているコードを書き換えるかどうかは別問題として、このような置き換え、というか読み替えをする上でのちょっとした注意点を紹介します。

非同期メソッドの基本動作

以前、非同期メソッド入門 (2) - async修飾子とawait演算子でも紹介したように、下記のコードは同じ挙動を示します。上が.NET Framework 4.5標準での記述、下が.NET Framework 4標準での記述です。

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());
}

await演算子以降は非同期処理が終わった後に実行される「継続処理」で、これをTaskを使って表現する場合はContinueWithの中に記述します。これが基本です。

単純な相互読み替えは危険

下記のような 実にヒドい コードがあるとします。これは、ボタンを押したらボタンのキャプションに毎秒現在時刻が表示される簡単なタイマーです。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    while (true)
    {
        await Task.Delay(1000);
        this.button.Content = DateTime.Now.ToLongTimeString();
    }
}

これを先のルールに従って単純にTask.ContinueWithを使ったコードに置き換えてみると下記のようになります。

private void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    while (true)
    {
        Task.Delay(1000).ContinueWith(_ =>
        {
            this.button.Content = DateTime.Now.ToLongTimeString();
        }, TaskScheduler.FromCurrentSynchronizationContext());
    }
}

たったこれだけのコードですが、このコードはフリーズします。「await演算子以降は継続処理になる」というのは「ループの次の処理も含めてメソッド内の残りの処理すべてが継続処理」として扱われます。Task.ContinueWithのコードは時間の更新のみを継続処理としており、ループの次の処理までは考慮されていません。Task.Delayを呼び出したら即座に次のループに回るので、単純にwhileループを連続して処理しているだけになります。そのため画面がフリーズしてしまうという結果になります。

これをちゃんと動くように実装するとすれば、例えば匿名メソッド再帰呼び出しを利用して以下のように書けます。難易度が急上昇したような気がしますが、async/awaitがあればこんな苦労も必要ありませんね。

private void Button_Click(object sender, RoutedEventArgs e)
{
    this.button.IsEnabled = false;
    Action action         = null;
    action = () =>
    {
        Task.Delay(1000).ContinueWith(_ =>
        {
            this.button.Content = DateTime.Now.ToLongTimeString();
            action();
        }, TaskScheduler.FromCurrentSynchronizationContext());
    };
    action();
}

まとめ

このように、非同期メソッドとTask.ContinueWithの読み替えには多少なりの注意が必要です。業務上でも、どうしても初めて利用/勉強する方への説明は最初の例ようなものになってしまいますが、単純な置換ではないことをしっかりフォローする必要があると感じています。簡単に非同期処理を実装できるようになった魔法のasync/awaitも、ちゃんと動きを理解しておかないとやっぱりハマります。これまでの非同期処理との違いをしっかり把握して、自由自在に使いこなせるようになりたいですね!