例外狀況處理 (工作平行程式庫)

由在工作中執行的使用者程式碼所擲回的未處理例外狀況,會傳播回到呼叫執行緒,本主題稍後所述的特定情況除外。 當您使用其中一個靜態或執行個體 Task.Wait 方法時會傳播例外狀況,您可以用 try/catch 陳述式括住呼叫來處理它們。 如果工作是已連結子工作的父代,或如果您在等候多個工作,就可能會擲回多個例外狀況。

若要將所有例外狀況傳播回呼叫執行緒,工作基礎結構會將它們包裝在 AggregateException 執行個體中。 AggregateException 例外狀況有 InnerExceptions 屬性,您可以列舉這個屬性來檢查所有擲回的原始例外狀況,並且個別處理 (或不處理) 每個例外狀況。 您也可以使用 AggregateException.Handle 方法處理原始的例外狀況。

即使只擲回一個例外狀況,它仍然會包裝在 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!

警告

上述範例程式碼包含 while 迴圈,會輪詢工作的 Task.IsCompleted 屬性,以判斷工作何時完成。 這絕對不應該在實際執行程式碼中完成,因為它非常沒有效率。

如果您不需要等候工作傳播例外狀況,或存取其 Exception 屬性,當工作回收時就會根據 .NET 例外狀況原則提昇例外狀況。

當允許例外狀況反昇至聯結的執行緒時,工作可能就可以在引發例外狀況之後,繼續處理某些項目。

注意

啟用 [Just My Code] 時,Visual Studio 在某些情況下會在擲回例外狀況的字行上中斷,並顯示錯誤訊息,指出「使用者程式碼未處理例外狀況」。這個錯誤是良性的。 您可以按 F5 繼續,並查看下面範例中示範的例外狀況處理行為。 若要防止 Visual Studio 在遇到第一個錯誤時就中斷,只要取消核取 [工具]、[選項]、[偵錯]、[一般] 下的 [ 啟用 Just My Code] 核取方塊即可。

已附加子工作和巢狀的 AggregateExceptions

如果工作有會擲回例外狀況的已連結子工作,該例外狀況就會先包裝在 AggregateException 再傳播到父工作,這會先把此例外狀況包裝在它自己的 AggregateException 再傳播回呼叫執行緒。 在這種情況下,在 Task.WaitWaitAnyWaitAll 方法攔截的 AggregateException 例外狀況,其 InnerExceptions 屬性會包含一個或多個 AggregateException 執行個體,而不是包含造成錯誤的原始例外狀況。 為免逐一查看巢狀 AggregateException 例外狀況,您可以使用 Flatten 方法移除所有的巢狀 AggregateException 例外狀況,讓 AggregateException.InnerExceptions 屬性包含原始例外狀況。 下例中,巢狀 AggregateException 執行個體被扁平化,並只在一個迴圈中處理。


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,而且在檢查內部例外狀況時可以看到它。 不過,如果呼叫執行緒並未等待工作,就不會傳播這個特定的例外狀況。 如需詳細資訊,請參閱 Task Cancellation

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)

使用控制方法篩選內部例外狀況

您可以使用 AggregateException.Handle 方法,不用任何進一步的邏輯,篩選出您可以視為「已處理」的例外狀況。 在提供給 AggregateException.Handle(Func<Exception,Boolean>) 方法的使用者委派中,您可以檢查例外狀況類型、其 Message 屬性或任何其他相關資訊,讓您判斷其是否為良性。 在 AggregateException.Handle 方法傳回之後,任何讓委派傳回 false 的例外狀況都會立即在新的 AggregateException 執行個體中重新擲回。

下例在功能上等同於本主題的第一個範例,這會檢查 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 屬性來觀察例外狀況。

重要

使用下列運算式時,無法明確攔截 AggregateException

  • await task
  • task.GetAwaiter().GetResult()

UnobservedTaskException 事件

在某些情況下,例如裝載時不受信任的外掛程式,良性的例外狀況可能很常見,而要手動觀察全部太困難。 在這些情況下,您可以處理 TaskScheduler.UnobservedTaskException 事件。 傳遞至處理常式的 System.Threading.Tasks.UnobservedTaskExceptionEventArgs 執行個體,可用來防止未觀察到的例外狀況傳播回聯結執行緒。

另請參閱