async/awaitキーワードを利用することで、非同期処理を簡潔に記述できる。ただし、それらをtry〜catch文で例外処理する際には注意すべき点もある。
async/awaitキーワードは、Visual Studio 2012+.NET Framework 4.5から利用可能になった、非同期処理の糖衣構文である。async/awaitのコードは、一般的にはTPL(タスク並列ライブラリ)を使ったコードに展開される(TPLでなくともGetAwaiterメソッドを実装していればawaitできる)。
async/awaitとTPLによって非同期処理が簡潔に書けるようになり、非同期処理が身近なものになった。とはいうものの、非同期処理に特有の注意点はある。本稿では、awaitを使う方法を中心に、TPLをそのまま使って待機する方法も含めて、例外をキャッチする方法について解説する。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017(15.3以降)が必要である。サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。
using System;
using System.Threading.Tasks;
using static System.Console;
Imports System.Console
最初に、以降のサンプルコードから呼び出す非同期メソッドを掲載しておこう(次のコード)。
static async Task SampleMethod1Async()
{
await Task.Delay(200);
WriteLine("SampleMethod1Asyncで例外を発生");
throw new InvalidOperationException("SampleMethod1Asyncの例外");
}
static async Task SampleMethod2Async()
{
await Task.Delay(100);
WriteLine("SampleMethod2Asyncで例外を発生");
throw new InvalidOperationException("SampleMethod2Asyncの例外");
}
static async void SampleMethod3Async()
{
await Task.Delay(100);
WriteLine("SampleMethod3Asyncで例外を発生");
throw new InvalidOperationException("SampleMethod3Asyncの例外");
}
Async Function SampleMethod1Async() As Task
Await Task.Delay(200)
WriteLine("SampleMethod1Asyncで例外を発生")
Throw New InvalidOperationException("SampleMethod1Asyncの例外")
End Function
Async Function SampleMethod2Async() As Task
Await Task.Delay(100)
WriteLine("SampleMethod2Asyncで例外を発生")
Throw New InvalidOperationException("SampleMethod2Asyncの例外")
End Function
Async Sub SampleMethod3Async()
Await Task.Delay(100)
WriteLine("SampleMethod3Asyncで例外を発生")
Throw New InvalidOperationException("SampleMethod3Asyncの例外")
End Sub
タスクを並列に実行しない場合、話はシンプルだ。普通にtry〜catchすれば、awaitでは想定通りの例外がそのままキャッチされる。TPLのTaskクラス(System.Threading.Tasks名前空間)のWaitメソッドで待機した場合は、タスクで発生した例外がAggregateException例外(System名前空間)にラップされ、それがキャッチされる。
まず、awaitの例を示す(次のコード)。普通にtry〜catchすることで、予期したInvalidOperationException例外(System名前空間)がそのままキャッチされている。
static async Task Main(string[] args)
{
try
{
await SampleMethod1Async();
// 出力:SampleMethod1Asyncで例外を発生
await SampleMethod2Async(); // この行は実行されない
}
catch (InvalidOperationException e)
{
WriteLine($"{e.GetType().Name} - {e.Message}");
// 出力:InvalidOperationException - SampleMethod1Asyncの例外
}
#if DEBUG
ReadKey();
#endif
}
Sub Main()
Dim mainTask = Task.Run(
Async Function()
Try
Await SampleMethod1Async()
' 出力:SampleMethod1Asyncで例外を発生
Await SampleMethod2Async() ' この行は実行されない
Catch e As InvalidOperationException
WriteLine($"{e.GetType().Name} - {e.Message}")
' 出力:InvalidOperationException - SampleMethod1Asyncの例外
End Try
End Function)
mainTask.Wait()
#If DEBUG Then
ReadKey()
#End If
End Sub
上のサンプルコードではawaitを2回使っているが、その2つのタスクは並列ではなく直列に実行される。つまり、1つ目のタスクが完了してから、2つ目のタスクが始まるのである。そのため、1つ目のタスクの実行中に例外が発生すると、2つ目のタスクは実行されない。なお、finally句を書いた場合であるが、このようにawaitしたり、次の例のようにWaitメソッドを使ったりしてタスクの終了を待機しているときは、タスクが終了してから(あるいは、例外によってcatch句が実行されてから)finally句が実行される。
次に、TaskクラスのWaitメソッドで待機した場合だ(次のコード)。タスクで発生したInvalidOperationException例外は、そのままの形ではキャッチされない。AggregateException例外にラップされた状態でキャッチされるので、実際に発生した例外を知るために、AggregateException例外のInnerExceptionsプロパティに含まれるExceptionオブジェクトを列挙している。
var task1 = SampleMethod1Async();
try
{
task1.Wait();
// 出力:SampleMethod1Asyncで例外を発生
}
catch (AggregateException ae)
{
WriteLine($"{ae.GetType().Name} - {ae.Message}");
// 出力:AggregateException - 1 つ以上のエラーが発生しました。
foreach (Exception e in ae.InnerExceptions)
WriteLine($"{e.GetType().Name} - {e.Message}");
// 出力:InvalidOperationException - SampleMethod1Asyncの例外
}
Dim task1 = SampleMethod1Async()
Try
task1.Wait()
' 出力:SampleMethod1Asyncで例外を発生
Catch ae As AggregateException
WriteLine($"{ae.GetType().Name} - {ae.Message}")
' 出力:AggregateException - 1 つ以上のエラーが発生しました。
For Each e As Exception In ae.InnerExceptions
WriteLine($"{e.GetType().Name} - {e.Message}")
' 出力:InvalidOperationException - SampleMethod1Asyncの例外
Next
End Try
awaitではそのまま例外が出てくる。WaitするとAggregateExceptionになって出てくる。まずはこのことをしっかり押さえておいてほしい。
awaitもWaitもしないときは、タスクで発生した例外はどうなるのだろうか? 答えは、「try〜catchではキャッチできない」である(次のコード)。
返り値を持たない非同期メソッドはawaitできない。もしもそのメソッドから例外が送出されると、例外をキャッチできずにプログラムは異常終了してしまう。逆にいうと、返り値を持たない非同期メソッドを書くときは、全ての例外をメソッド内で処理するようにすべきだということである。
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
var ex = e.ExceptionObject as Exception;
WriteLine($"UnhandledException - {ex.Message}");
// 出力:UnhandledException - SampleMethod3Asyncの例外
#if DEBUG
ReadKey();
#endif
Environment.Exit(1); // プログラム終了
};
try
{
SampleMethod3Async();
// 出力:SampleMethod3Asyncで例外を発生
}
catch (Exception e)
{
WriteLine($"{e.GetType().Name} - {e.Message}");
// ↑この行は実行されない
}
AddHandler AppDomain.CurrentDomain.UnhandledException,
Sub(s, e)
Dim ex = DirectCast(e.ExceptionObject, Exception)
WriteLine($"UnhandledException - {ex.Message}")
' 出力:UnhandledException - SampleMethod3Asyncの例外
#If DEBUG Then
ReadKey()
#End If
Environment.Exit(1) ' プログラム終了
End Sub
Try
SampleMethod3Async()
' 出力:SampleMethod3Asyncで例外を発生
Catch e As Exception
WriteLine($"{e.GetType().Name} - {e.Message}")
' ↑この行は実行されない
End Try
複数のタスクを並列実行する場合は、例外処理で考慮すべきことが増える。
例外が出たかどうかを知りたいだけなら、Task.WhenAllをawaitするところで普通にtry〜catchすればよい(次のコード)。
複数のタスクを並列実行したときに全てのタスクの終了を待機するには、それぞれの非同期メソッドが返してきたTaskオブジェクトを引数としてTaskクラスのWhenAllメソッドを呼び出し、そのWhenAllメソッドが返してきたTaskオブジェクトを待機する。そのときawaitを使うと、try〜catchすれば前述したように例外がそのままキャッチできる。ただし、直接キャッチできるのは1つだけである。並列に実行させた複数のタスクから複数の例外が発生したとしても、そのうちの1つしかキャッチできないのだ。それでも、例外が出たかどうかを知りたいだけならば、このようなコードでも十分であろう。
var task1 = SampleMethod1Async();
var task2 = SampleMethod2Async();
try
{
await Task.WhenAll(task1, task2);
// 出力:SampleMethod2Asyncで例外を発生
// 出力:SampleMethod1Asyncで例外を発生
}
catch (InvalidOperationException e)
{
WriteLine($"{e.GetType().Name} - {e.Message}");
// 出力:InvalidOperationException - SampleMethod1Asyncの例外
}
Dim task1 = SampleMethod1Async()
Dim task2 = SampleMethod2Async()
Try
Await Task.WhenAll(task1, task2)
' 出力:SampleMethod2Asyncで例外を発生
' 出力:SampleMethod1Asyncで例外を発生
Catch e As InvalidOperationException
WriteLine($"{e.GetType().Name} - {e.Message}")
' 出力:InvalidOperationException - SampleMethod1Asyncの例外
End Try
それでは、複数のタスクを並列実行したときに全ての例外を知るには、どうしたらよいだろうか? 2通りの方法がある。
まず、awaitをやめて、Waitする方法だ(次のコード)。Waitすれば、全ての例外が1つのAggregateException例外にラップされて出てくる。待機しているときにスレッドをブロックしても構わなければ、この方法が簡単だろう。
var task1 = SampleMethod1Async();
var task2 = SampleMethod2Async();
try
{
Task.WhenAll(task1, task2).Wait();
// 出力:SampleMethod2Asyncで例外を発生
// 出力:SampleMethod1Asyncで例外を発生
}
catch (AggregateException ae)
{
WriteLine($"{ae.GetType().Name} - {ae.Message}");
// 出力:AggregateException - 1 つ以上のエラーが発生しました。
foreach (Exception e in ae.InnerExceptions)
WriteLine($"{e.GetType().Name} - {e.Message}");
// 出力:InvalidOperationException - SampleMethod1Asyncの例外
// 出力:InvalidOperationException - SampleMethod2Asyncの例外
}
Dim task1 = SampleMethod1Async()
Dim task2 = SampleMethod2Async()
Try
Task.WhenAll(task1, task2).Wait()
' 出力:SampleMethod2Asyncで例外を発生
' 出力:SampleMethod1Asyncで例外を発生
Catch ae As AggregateException
WriteLine($"{ae.GetType().Name} - {ae.Message}")
' 出力:AggregateException - 1 つ以上のエラーが発生しました。
For Each e As Exception In ae.InnerExceptions
WriteLine($"{e.GetType().Name} - {e.Message}")
' 出力:InvalidOperationException - SampleMethod1Asyncの例外
' 出力:InvalidOperationException - SampleMethod2Asyncの例外
Next
End Try
もう1つは、awaitするタスクをローカル変数に保持しておいて、キャッチブロックでそのExceptionプロパティを見る方法だ(次のコード)。TaskオブジェクトのExceptionプロパティはAggregateException例外になっていて、そのInnerExceptionsプロパティを列挙すれば、発生した全ての例外が分かる。
var task1 = SampleMethod1Async();
var task2 = SampleMethod2Async();
var allTasks = Task.WhenAll(task1, task2);
try
{
await allTasks;
// 出力:SampleMethod2Asyncで例外を発生
// 出力:SampleMethod1Asyncで例外を発生
}
catch (Exception ex)
{
WriteLine($"[ex] {ex.GetType().Name} - {ex.Message}");
// 出力:[ex] InvalidOperationException - SampleMethod1Asyncの例外
foreach (Exception e in allTasks.Exception.InnerExceptions)
WriteLine($"[InnerExceptions] {e.GetType().Name} - {e.Message}");
// 出力:[InnerExceptions] InvalidOperationException - SampleMethod1Asyncの例外
// 出力:[InnerExceptions] InvalidOperationException - SampleMethod2Asyncの例外
}
Dim task1 = SampleMethod1Async()
Dim task2 = SampleMethod2Async()
Dim allTasks = Task.WhenAll(task1, task2)
Try
Await allTasks
' 出力:SampleMethod2Asyncで例外を発生
' 出力:SampleMethod1Asyncで例外を発生
Catch ex As Exception
WriteLine($"[ex] {ex.GetType().Name} - {ex.Message}")
' 出力:[ex] InvalidOperationException - SampleMethod1Asyncの例外
For Each e As Exception In allTasks.Exception.InnerExceptions
WriteLine($"[InnerExceptions] {e.GetType().Name} - {e.Message}")
' 出力:[InnerExceptions] InvalidOperationException - SampleMethod1Asyncの例外
' 出力:[InnerExceptions] InvalidOperationException - SampleMethod2Asyncの例外
Next
End Try
awaitしたとき、普通にtry〜catchすれば非同期メソッドで発生した例外もキャッチできる。ただし、複数のタスクを並列実行する場合に、発生した全ての例外を知るには工夫が必要になる。
利用可能バージョン:Visual Studio 2012以降
カテゴリ:C# 処理対象:言語構文
カテゴリ:Visual Basic .NET 処理対象:言語構文
カテゴリ:クラスライブラリ 処理対象:非同期処理
関連TIPS:適切に処理されなかった例外をキャッチするには?
関連TIPS:構文:キャッチした例外をリスローするには?[C#/VB]
関連TIPS:WPF:例外をまとめてトラップするには?[C#/VB]
関連TIPS:C# 7のローカル関数の使いどころとは?
関連TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)
関連TIPS:非同期:awaitを含むコードをロックするには?(AsyncLock編)
関連TIPS:ループをParallelクラスで並列処理にするには?[C#/VB]
関連TIPS:WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?(async/await編)[C#/VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
Copyright© Digital Advantage Corp. All Rights Reserved.