連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門

第2回 非同期メソッドの構文

鈴木 孝明
2012/09/25
Page1 Page2 Page3

例外処理

 非同期処理中に発生した例外をどう処理するか。きっと考えただけでも面倒に感じることだろう。しかし、非同期メソッドでは例外処理も非常に簡単に行える。それでは、どう記述するのかを見ていこう。

通常と同じ記述の例外処理

 実はList 6、List 7に対比するように、例外処理についても同期コードと全く同じ書き方で対応できる。これまでAPMやEAPなどで体験した苦労や煩雑さはなく、可読性に優れた記述ができる。

private void Button_Click(object sender, RoutedEventArgs e)
{
  try
  {
    this.button.IsEnabled = false;
    throw new Exception();
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    this.button.IsEnabled = true;
  }
}
Private Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Try
    Me.button.IsEnabled = False
    Throw New Exception()
  Catch ex As Exception
    MessageBox.Show(ex.Message)
  Finally
    Me.button.IsEnabled = True
  End Try

End Sub
List 6: 同期メソッドでの例外処理(上:C#、下:VB)

 

private async void Button_Click(object sender, RoutedEventArgs e)
{
  try
  {
    this.button.IsEnabled = false;
    await Task.Run(() => { throw new Exception(); });
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
  finally
  {
    this.button.IsEnabled = true;
  }
}
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Try
    Me.button.IsEnabled = False
    Await Task.Run(
      Sub()
        Throw New Exception()
      End Sub)
  Catch ex As Exception
    MessageBox.Show(ex.Message)
  Finally
    Me.button.IsEnabled = True
  End Try

End Sub
List 7: 非同期メソッドでの例外処理(上:C#、下:VB)

通知される例外は最初の1つだけ

 Task(=Taskオブジェクト内部で実行される非同期操作)の中で発生したが処理されなかった例外は、Taskクラス自身によって捕捉され、AggregateException型(System名前空間)にラップされてスローされる。これは、List 8の8行目(=Task.WhenAllメソッド)のように複数のTaskを1つのTaskにまとめた場合など、複数の例外が同時に発生することが考慮されているためだ。

 List 8を実行すると、AggregateException型を介して全ての例外を取得できる。

private static void DoSomething()
{
  try
  {
    var task1 = Task.Run(() => { throw new InvalidCastException();  });
    var task2 = Task.Run(() => { throw new ArgumentException();     });
    var task3 = Task.Run(() => { throw new NotSupportedException(); });
    var task  = Task.WhenAll(task1, task2, task3);
    task.Wait(); // 例外をスロー
  }
  catch (AggregateException ex)
  {
    Console.WriteLine(ex.GetType());
    foreach (var inner in ex.InnerExceptions)
      Console.WriteLine("\t{0}", inner.GetType());
  }
}

/*
  System.AggregateException
  System.InvalidCastException
  System.ArgumentException
  System.NotSupportedException
*/
Private Shared Sub DoSomething()

  Try
    Dim task1 = Task.Run(Sub()
                           Throw New InvalidCastException()
                         End Sub)
    Dim task2 = Task.Run(Sub()
                           Throw New ArgumentException()
                         End Sub)
    Dim task3 = Task.Run(Sub()
                           Throw New NotSupportedException()
                         End Sub)
    Dim taskAll = Task.WhenAll(task1, task2, task3)
    taskAll.Wait() ' 例外をスロー

  Catch ex As AggregateException
    Console.WriteLine(ex.GetType())
    For Each inner In ex.InnerExceptions
      Console.WriteLine("\t{0}", inner.GetType())
    Next
  End Try

End Sub

'System.AggregateException
'System.InvalidCastException
'System.ArgumentException
'System.NotSupportedException
List 8: Taskから発行される例外処理(上:C#、下:VB)

 しかし、List 9のように合成したTaskをawaitすると、異なる結果が得られる。

private static async void DoSomething()
{
  try
  {
      var task1 = Task.Run(() => { throw new InvalidCastException();  });
      var task2 = Task.Run(() => { throw new ArgumentException();     });
      var task3 = Task.Run(() => { throw new NotSupportedException(); });
      await Task.WhenAll(task1, task2, task3);
  }
  catch (AggregateException)
  {
    // ここは呼び出されない
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.GetType());
  }
}

/*
  System.InvalidCastException
*/
Private Shared Async Sub DoSomething()

  Try
    Dim task1 = Task.Run(Sub()
                           Throw New InvalidCastException()
                         End Sub)
    Dim task2 = Task.Run(Sub()
                           Throw New ArgumentException()
                         End Sub)
    Dim task3 = Task.Run(Sub()
                           Throw New NotSupportedException()
                         End Sub)
    Await Task.WhenAll(task1, task2, task3)

  Catch ex As AggregateException
    ' ここは呼び出されない
  Catch ex As Exception
    Console.WriteLine(ex.GetType())
  End Try

End Sub

'System.InvalidCastException
List 9: awaitから発行される例外処理(上:C#、下:VB)

 このように、複数の例外が存在する場合でもawaitから発行される例外は最初の1つだけとなり、AggregateException型ではないので注意が必要だ。

UIスレッドに制御を戻すかどうかをカスタマイズする

 List 2でも紹介したように、非同期メソッドはUIスレッドに同期的な継続処理に自動変換される。この変換のおかげで、開発者は特に意識することなく、ボタンなどのUI要素に触れることができる。このようにスレッド間の移動が隠ぺいされることは、実装が非常に簡単になるため、歓迎すべきことだ。

 しかしながら、必ずしも継続処理をUIスレッドに戻す必要がないケースもあるだろう。このようなときのために、UIスレッドに制御を戻すかどうかをカスタマイズする方法が用意されている。List 10にその例を示す。

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スレッド上でないので例外が発生する
}
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  Await Task.Run(
    Sub()
      Thread.Sleep(3000)
    End Sub).ConfigureAwait(False)
  Me.button.IsEnabled = True ' UIスレッド上でないので例外が発生する

End Sub
List 10: 継続処理をUIスレッドに戻さない(上:C#、下:VB)

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

 言わずもがなかもしれないが、List 10はList 11のような意味になる。

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.button.IsEnabled = false;
  HeavyWork().ContinueWith(_ =>  // UIに非同期的な継続処理
  {
    this.button.IsEnabled = true;
  });  // TaskSchedulerを設定しない
}
Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

  Me.button.IsEnabled = False
  ' UIに非同期的な継続処理
  HeavyWork().ContinueWith(
    Sub(_)
      Me.button.IsEnabled = True
    End Sub ' TaskSchedulerを設定しない

End Sub
List 11: 継続渡しへの変換(上:C#、下:VB)

まとめ

 ここまで非同期メソッドの構文や使い方、注意点などについて解説してきた。その挙動は非同期処理だが、記述の仕方が同期処理と非常に似通っていることが以前にも増して分かったのではないかと思う。むしろ、あまりに似通っているので本当に非同期処理をしているのか不安になるかもしれない。しかし、その分、非同期処理がより身近に感じられるのではないだろうか。何度も体験することで徐々に慣れていくと思うので、非同期処理に苦手意識がある方は、ぜひどんどんトライしてみてほしい。非同期メソッドの記法には、多くの開発者に非同期処理を容易に体得させるだけのパワーがある。

 さて、次回は非同期メソッドの内部実装に踏み込み、Awaitableパターンの独自実装について解説しようと思う。非常にディープな内容になるが、ぜひ楽しみに待っていてほしい。それでは、また次回。end of article

 

 

 INDEX
  連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
  第2回 非同期メソッドの構文
    1.非同期メソッドのキーワード/非同期メソッドの動き
    2.任意の記述場所/戻り値
  3.例外処理/UIスレッドに制御を戻すかどうかをカスタマイズする

 

インデックス・ページヘ  「連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)
- PR -

注目のテーマ

業務アプリInsider 記事ランキング

本日 月間
ソリューションFLASH