例外処理 (タスク並列ライブラリ)
タスク内で実行中のユーザー コードによってスローされた、ハンドルされない例外は、呼び出し元のスレッドに反映されます。ただし、このトピックの後半で説明している特定の状況を除きます。 静的な、またはインスタンスの Task.Wait メソッドの 1 つを使用し、その呼び出しを try
/catch
ステートメント内に入れて例外を処理すると、例外が反映されます。 タスクが、アタッチされた子タスクの親である場合、または複数のタスクを待機している場合、複数の例外がスローされることがあります。
呼び出し元のスレッドにすべての例外を反映するために、Task インフラストラクチャが例外を AggregateException インスタンスにラップします。 AggregateException 例外には、スローされた元のすべての例外を調べるために列挙できる InnerExceptions プロパティがあり、個々に処理したり未処理にしたりできます。 また、 AggregateException.Handle メソッドを使用して元の例外を処理することもできます。
例外が 1 つだけスローされた場合でも、次の例のように AggregateException 例外にラップされます。
public static partial class Program
{
public static void HandleThree()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
// Rethrow any other exception.
else
{
throw ex;
}
}
}
}
}
// The example displays the following output:
// This exception is expected!
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
' Handle the custom exception.
If TypeOf ex Is CustomException Then
Console.WriteLine(ex.Message)
' Rethrow any other exception.
Else
Throw ex
End If
Next
End Try
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays the following output:
' This exception is expected!
ハンドルされない例外は、 AggregateException をキャッチして、内部例外を確認しないことで回避できます。 ただし、並列でない状況で基本的な Exception の種類をキャッチする場合と類似しているため、この操作はお勧めしません。 特定の操作を取得することなく例外をキャッチして回復しようとすると、プログラムが中間状態のままになるおそれがあります。
Task.Wait メソッドを呼び出してタスクの完了を待機する処理を行わない場合は、次の例のようにタスクの Exception プロパティから AggregateException 例外を取得することもできます。 詳細については、このトピックの「Task.Exception プロパティによる例外の確認」セクションを参照してください。
public static partial class Program
{
public static void HandleFour()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
while (!task.IsCompleted) { }
if (task.Status == TaskStatus.Faulted)
{
foreach (var ex in task.Exception?.InnerExceptions ?? new(Array.Empty<Exception>()))
{
// Handle the custom exception.
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
// Rethrow any other exception.
else
{
throw ex;
}
}
}
}
}
// The example displays the following output:
// This exception is expected!
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))
While Not task1.IsCompleted
End While
If task1.Status = TaskStatus.Faulted Then
For Each ex In task1.Exception.InnerExceptions
' Handle the custom exception.
If TypeOf ex Is CustomException Then
Console.WriteLine(ex.Message)
' Rethrow any other exception.
Else
Throw ex
End If
Next
End If
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays the following output:
' This exception is expected!
注意事項
前のコード例には、タスクの Task.IsCompleted プロパティをポーリングして、タスクがいつ完了したかを判断する while
ループが含まれています。 これは非常に非効率的なので、運用環境のコードでは実行しないでください。
例外を反映するタスクを待機しない場合、またはタスクの Exception プロパティにアクセスする場合、例外はタスクがガベージ コレクトされるときに .NET の例外ポリシーに従ってエスカレートされます。
連結しているスレッドへ例外が上方向に通知されると、例外が発生した後も、タスクによって一部の項目の処理が続行される可能性があります。
注意
[マイ コードのみ] が有効になっている場合、Visual Studio では、例外をスローする行で処理が中断され、"ユーザー コードで処理されない例外" に関するエラー メッセージが表示されることがあります。このエラーは問題にはなりません。 F5 キーを押して続行し、以下の例に示す例外処理動作を確認できます。 Visual Studio による処理が最初のエラーで中断しないようにするには、 [ツール] メニューの [オプション]、[デバッグ] の順にクリックし、[全般] で [マイ コードのみを有効にする] チェック ボックスをオフにします。
アタッチされた子タスクと入れ子の AggregateExceptions
タスクに、例外をスローする子タスクがアタッチされている場合、その例外は親タスクに反映される前に AggregateException でラップされます。つまり、呼び出し元のスレッドに反映される前に、その例外が固有の AggregateException でラップされるということです。 このような場合、Task.Wait、WaitAny、WaitAll の各メソッドでキャッチされた AggregateException 例外の InnerExceptions プロパティには、違反の原因となった元の例外ではなく、1 つ以上の AggregateException インスタンスが含まれます。 入れ子の AggregateException 例外を反復処理しなくて済むようにするには、Flatten メソッドを使用して入れ子の AggregateException をすべて削除します。これにより、AggregateException.InnerExceptions プロパティに元の例外が含まれるようになります。 次の例では、入れ子の AggregateException インスタンスが 1 つのループで平坦化され、処理されています。
public static partial class Program
{
public static void FlattenTwo()
{
var task = Task.Factory.StartNew(() =>
{
var child = Task.Factory.StartNew(() =>
{
var grandChild = Task.Factory.StartNew(() =>
{
// This exception is nested inside three AggregateExceptions.
throw new CustomException("Attached child2 faulted.");
}, TaskCreationOptions.AttachedToParent);
// This exception is nested inside two AggregateExceptions.
throw new CustomException("Attached child1 faulted.");
}, TaskCreationOptions.AttachedToParent);
});
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var ex in ae.Flatten().InnerExceptions)
{
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
else
{
throw;
}
}
}
}
}
// The example displays the following output:
// Attached child1 faulted.
// Attached child2 faulted.
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Factory.StartNew(Sub()
Dim child1 = Task.Factory.StartNew(Sub()
Dim child2 = Task.Factory.StartNew(Sub()
Throw New CustomException("Attached child2 faulted.")
End Sub,
TaskCreationOptions.AttachedToParent)
Throw New CustomException("Attached child1 faulted.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.Flatten().InnerExceptions
If TypeOf ex Is CustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
End Try
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays the following output:
' Attached child1 faulted.
' Attached child2 faulted.
また、次の例のように、AggregateException.Flatten メソッドを使用して、単一の AggregateException インスタンス内の複数のタスクによってスローされた複数の AggregateException インスタンスから内部例外を再スローすることもできます。
public static partial class Program
{
public static void TaskExceptionTwo()
{
try
{
ExecuteTasks();
}
catch (AggregateException ae)
{
foreach (var e in ae.InnerExceptions)
{
Console.WriteLine(
"{0}:\n {1}", e.GetType().Name, e.Message);
}
}
}
static void ExecuteTasks()
{
// Assume this is a user-entered String.
string path = @"C:\";
List<Task> tasks = new();
tasks.Add(Task.Run(() =>
{
// This should throw an UnauthorizedAccessException.
return Directory.GetFiles(
path, "*.txt",
SearchOption.AllDirectories);
}));
tasks.Add(Task.Run(() =>
{
if (path == @"C:\")
{
throw new ArgumentException(
"The system root is not a valid path.");
}
return new string[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
}));
tasks.Add(Task.Run(() =>
{
throw new NotImplementedException(
"This operation has not been implemented.");
}));
try
{
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ae)
{
throw ae.Flatten();
}
}
}
// The example displays the following output:
// UnauthorizedAccessException:
// Access to the path 'C:\Documents and Settings' is denied.
// ArgumentException:
// The system root is not a valid path.
// NotImplementedException:
// This operation has not been implemented.
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Try
ExecuteTasks()
Catch ae As AggregateException
For Each e In ae.InnerExceptions
Console.WriteLine("{0}:{2} {1}", e.GetType().Name, e.Message,
vbCrLf)
Next
End Try
End Sub
Sub ExecuteTasks()
' Assume this is a user-entered String.
Dim path = "C:\"
Dim tasks As New List(Of Task)
tasks.Add(Task.Run(Function()
' This should throw an UnauthorizedAccessException.
Return Directory.GetFiles(path, "*.txt",
SearchOption.AllDirectories)
End Function))
tasks.Add(Task.Run(Function()
If path = "C:\" Then
Throw New ArgumentException("The system root is not a valid path.")
End If
Return {".txt", ".dll", ".exe", ".bin", ".dat"}
End Function))
tasks.Add(Task.Run(Sub()
Throw New NotImplementedException("This operation has not been implemented.")
End Sub))
Try
Task.WaitAll(tasks.ToArray)
Catch ae As AggregateException
Throw ae.Flatten()
End Try
End Sub
End Module
' The example displays the following output:
' UnauthorizedAccessException:
' Access to the path 'C:\Documents and Settings' is denied.
' ArgumentException:
' The system root is not a valid path.
' NotImplementedException:
' This operation has not been implemented.
デタッチされた子タスクの例外
既定では、子タスクはデタッチされた状態で作成されます。 デタッチされたタスクからスローされた例外は、処理されるか、直接の親に再スローされる必要があります。これらの例外は、アタッチされた子タスクが反映されるのとは異なる方法で、呼び出し元のスレッドに反映されます。 最上位の親では、デタッチされた子からの例外を手動で再スローすることで、 AggregateException にラップして、呼び出し元のスレッドに反映させることができます。
public static partial class Program
{
public static void DetachedTwo()
{
var task = Task.Run(() =>
{
var nestedTask = Task.Run(
() => throw new CustomException("Detached child task faulted."));
// Here the exception will be escalated back to the calling thread.
// We could use try/catch here to prevent that.
nestedTask.Wait();
});
try
{
task.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
{
if (e is CustomException)
{
Console.WriteLine(e.Message);
}
}
}
}
}
// The example displays the following output:
// Detached child task faulted.
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Run(Sub()
Dim nestedTask1 = Task.Run(Sub()
Throw New CustomException("Detached child task faulted.")
End Sub)
' Here the exception will be escalated back to joining thread.
' We could use try/catch here to prevent that.
nestedTask1.Wait()
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.Flatten().InnerExceptions
If TypeOf ex Is CustomException Then
' Recover from the exception. Here we just
' print the message for demonstration purposes.
Console.WriteLine(ex.Message)
End If
Next
End Try
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays the following output:
' Detached child task faulted.
子タスク内の例外を確認するために継続を使用する場合でも、親タスクによって例外を確認する必要があります。
他の処理との連携によるキャンセル処理を示す例外
タスク内のユーザー コードがキャンセル要求に応答する場合の正しいプロシージャは、その要求が伝えられたキャセル トークンに渡すために OperationCanceledException をスローすることです。 例外が反映される前に、タスク インスタンスによって、例外内のトークンがそのタスクが作成されたときに渡されたトークンと比較されます。 それらが同じである場合、タスクは TaskCanceledException でラップされた AggregateExceptionを反映します。これは、内部例外を調べると確認できます。 ただし、呼び出し元のスレッドがタスクを待機していない場合、このような特定の例外は反映されません。 詳細については、「タスクのキャンセル」をご覧ください。
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
CancellationToken ct = token;
while (someCondition)
{
// Do some work...
Thread.SpinWait(50_000);
ct.ThrowIfCancellationRequested();
}
},
token);
// No waiting required.
tokenSource.Dispose();
Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token
Dim task1 = Task.Factory.StartNew(Sub()
Dim ct As CancellationToken = token
While someCondition = True
' Do some work...
Thread.SpinWait(500000)
ct.ThrowIfCancellationRequested()
End While
End Sub,
token)
内部例外をフィルター処理する Handle メソッドの使用
AggregateException.Handle メソッドを使用すると、"処理済み" として扱うことのできる例外をフィルターで除外できます。追加のロジックを使用する必要はありません。 AggregateException.Handle(Func<Exception,Boolean>) メソッドに用意されているユーザー デリゲートでは、例外の種類、その Message プロパティ、またはその例外に問題がないかどうかを判別できるその他の情報を調べることができます。 デリゲートが false
を返す例外は、AggregateException メソッドが返された直後に新しい AggregateException.Handle インスタンスで再スローされます。
次の例は、機能的には、AggregateException.InnerExceptions コレクション内の各例外を調べるこのトピックの最初の例と同じです。 代わりに、この例外ハンドラーでは、例外ごとに AggregateException.Handle メソッドのオブジェクトを呼び出し、CustomException
インスタンスでない例外のみを再スローしています。
public static partial class Program
{
public static void HandleMethodThree()
{
var task = Task.Run(
() => throw new CustomException("This exception is expected!"));
try
{
task.Wait();
}
catch (AggregateException ae)
{
// Call the Handle method to handle the custom exception,
// otherwise rethrow the exception.
ae.Handle(ex =>
{
if (ex is CustomException)
{
Console.WriteLine(ex.Message);
}
return ex is CustomException;
});
}
}
}
// The example displays the following output:
// This exception is expected!
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))
Try
task1.Wait()
Catch ae As AggregateException
' Call the Handle method to handle the custom exception,
' otherwise rethrow the exception.
ae.Handle(Function(e)
If TypeOf e Is CustomException Then
Console.WriteLine(e.Message)
End If
Return TypeOf e Is CustomException
End Function)
End Try
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays the following output:
' This exception is expected!
次の例はさらに複雑です。AggregateException.Handle メソッドを使用して、ファイルの列挙時に UnauthorizedAccessException 例外の特別な処理を行っています。
public static partial class Program
{
public static void TaskException()
{
// This should throw an UnauthorizedAccessException.
try
{
if (GetAllFiles(@"C:\") is { Length: > 0 } files)
{
foreach (var file in files)
{
Console.WriteLine(file);
}
}
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
{
Console.WriteLine(
"{0}: {1}", ex.GetType().Name, ex.Message);
}
}
Console.WriteLine();
// This should throw an ArgumentException.
try
{
foreach (var s in GetAllFiles(""))
{
Console.WriteLine(s);
}
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(
"{0}: {1}", ex.GetType().Name, ex.Message);
}
}
static string[] GetAllFiles(string path)
{
var task1 =
Task.Run(() => Directory.GetFiles(
path, "*.txt",
SearchOption.AllDirectories));
try
{
return task1.Result;
}
catch (AggregateException ae)
{
ae.Handle(x =>
{
// Handle an UnauthorizedAccessException
if (x is UnauthorizedAccessException)
{
Console.WriteLine(
"You do not have permission to access all folders in this path.");
Console.WriteLine(
"See your network administrator or try another path.");
}
return x is UnauthorizedAccessException;
});
return Array.Empty<string>();
}
}
}
// The example displays the following output:
// You do not have permission to access all folders in this path.
// See your network administrator or try another path.
//
// ArgumentException: The path is not of a legal form.
Imports System.IO
Imports System.Threading.Tasks
Module Example
Public Sub Main()
' This should throw an UnauthorizedAccessException.
Try
Dim files = GetAllFiles("C:\")
If files IsNot Nothing Then
For Each file In files
Console.WriteLine(file)
Next
End If
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
Next
End Try
Console.WriteLine()
' This should throw an ArgumentException.
Try
For Each s In GetAllFiles("")
Console.WriteLine(s)
Next
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
Next
End Try
Console.WriteLine()
End Sub
Function GetAllFiles(ByVal path As String) As String()
Dim task1 = Task.Run(Function()
Return Directory.GetFiles(path, "*.txt",
SearchOption.AllDirectories)
End Function)
Try
Return task1.Result
Catch ae As AggregateException
ae.Handle(Function(x)
' Handle an UnauthorizedAccessException
If TypeOf x Is UnauthorizedAccessException Then
Console.WriteLine("You do not have permission to access all folders in this path.")
Console.WriteLine("See your network administrator or try another path.")
End If
Return TypeOf x Is UnauthorizedAccessException
End Function)
End Try
Return Array.Empty(Of String)()
End Function
End Module
' The example displays the following output:
' You do not have permission to access all folders in this path.
' See your network administrator or try another path.
'
' ArgumentException: The path is not of a legal form.
Task.Exception プロパティによる例外の確認
タスクが TaskStatus.Faulted 状態で完了した場合、その Exception プロパティを調べることで違反の原因となった例外を見つけることができます。 Exception プロパティを確認する場合は、次の例に示すように、継続元タスクが違反した場合にのみ実行される継続を使用することをお勧めします。
public static partial class Program
{
public static void ExceptionPropagationTwo()
{
_ = Task.Run(
() => throw new CustomException("task1 faulted."))
.ContinueWith(_ =>
{
if (_.Exception?.InnerException is { } inner)
{
Console.WriteLine("{0}: {1}",
inner.GetType().Name,
inner.Message);
}
},
TaskContinuationOptions.OnlyOnFaulted);
Thread.Sleep(500);
}
}
// The example displays output like the following:
// CustomException: task1 faulted.
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim task1 = Task.Factory.StartNew(Sub()
Throw New CustomException("task1 faulted.")
End Sub).
ContinueWith(Sub(t)
Console.WriteLine("{0}: {1}",
t.Exception.InnerException.GetType().Name,
t.Exception.InnerException.Message)
End Sub, TaskContinuationOptions.OnlyOnFaulted)
Thread.Sleep(500)
End Sub
End Module
Class CustomException : Inherits Exception
Public Sub New(s As String)
MyBase.New(s)
End Sub
End Class
' The example displays output like the following:
' CustomException: task1 faulted.
重要なアプリケーションでは、継続のデリゲートで例外に関する詳細情報を記録して、新しいタスクを作成して例外から回復することが考えられます。 タスクで障害が発生した場合は、次の式によって例外をスローします。
await task
task.Wait()
task.Result
task.GetAwaiter().GetResult()
スローされた例外を処理したり観察したりするには、try-catch
ステートメントを使用します。 Task.Exception プロパティにアクセスして例外を観察する方法もあります。
UnobservedTaskException イベント
信頼関係のないプラグインをホストするときなど、シナリオによっては、問題のない例外がよく発生する場合や、難しすぎてすべてを手動で観察できなくなる場合があります。 このような場合、 TaskScheduler.UnobservedTaskException イベントを処理できます。 ハンドラーに渡される System.Threading.Tasks.UnobservedTaskExceptionEventArgs インスタンスを使用して、観察されない例外が連結しているスレッドに反映されないようにすることができます。
関連項目
.NET