連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門
第2回 非同期メソッドの構文
鈴木 孝明 2012/09/25 |
|
|
■例外処理
非同期処理中に発生した例外をどう処理するか。きっと考えただけでも面倒に感じることだろう。しかし、非同期メソッドでは例外処理も非常に簡単に行える。それでは、どう記述するのかを見ていこう。
●通常と同じ記述の例外処理
実は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パターンの独自実装について解説しようと思う。非常にディープな内容になるが、ぜひ楽しみに待っていてほしい。それでは、また次回。
業務アプリInsider 記事ランキング
本日
月間