Condividi tramite


Gestione delle eccezioni (Task Parallel Library)

Le eccezioni non gestite generate dal codice utente in esecuzione all'interno di un'attività vengono propagate nuovamente al thread chiamante, ad eccezione di alcuni scenari descritti più avanti in questo argomento. Le eccezioni vengono propagate quando si usa uno dei metodi statici o dell'istanza Task.Wait e le si gestiscono racchiudendo la chiamata in un'istruzionetry/catch . Se un'attività è l'elemento padre di attività figlio associate o se si è in attesa di più attività, potrebbero essere generate più eccezioni.

Per propagare tutte le eccezioni al thread chiamante, l'infrastruttura di attività le racchiude in un'istanza di AggregateException. L'eccezione AggregateException ha una proprietà InnerExceptions che può essere enumerata per esaminare tutte le eccezioni originali sollevate e gestire (o non gestire) ognuna singolarmente. È anche possibile gestire le eccezioni originali usando il AggregateException.Handle metodo .

Anche se viene generata solo un'eccezione, è comunque racchiusa in un'eccezione AggregateException, come illustrato nell'esempio seguente.


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!

È possibile evitare un'eccezione non gestita semplicemente rilevando AggregateException e non osservando alcuna delle eccezioni interne. Tuttavia, è consigliabile non eseguire questa operazione perché è analogo a intercettare il tipo di base Exception in scenari non paralleli. Per intercettare un'eccezione senza eseguire azioni specifiche per riprendersi da essa, si può lasciare il programma in uno stato indeterminato.

Se non si desidera chiamare il metodo per attendere il Task.Wait completamento di un'attività, è anche possibile recuperare l'eccezione AggregateException dalla proprietà dell'attività Exception , come illustrato nell'esempio seguente. Per ulteriori informazioni, vedere la sezione Osservare le eccezioni utilizzando la proprietà Task.Exception in questo contesto.


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!

Attenzione

Il codice di esempio precedente include un while ciclo che controlla la proprietà dell'attività Task.IsCompleted per determinare quando l'attività è stata completata. Questa operazione non deve mai essere eseguita nel codice di produzione perché è molto inefficiente.

Se non si attende un'attività che propaga un'eccezione o si accede alla relativa proprietà Exception, l'eccezione viene escalata in base ai criteri di eccezioni di .NET quando l'attività viene sottoposta al processo di Garbage Collection.

Quando le eccezioni possono risalire fino al thread di join, è possibile che un'attività continui a elaborare alcuni elementi dopo che l'eccezione è stata sollevata.

Nota

Quando "Just My Code" è abilitato, Visual Studio in alcuni casi si interromperà sulla riga che genera l'eccezione e visualizza un messaggio di errore che indica che l'eccezione non è gestita dal codice utente. Questo errore non è dannoso. È possibile premere F5 per continuare e visualizzare il comportamento di gestione delle eccezioni illustrato in questi esempi. Per impedire l'interruzione di Visual Studio al primo errore, deselezionare la casella di controllo Abilita Just My Code in Strumenti, Opzioni, Debug, Generale.

Attività figlio associate e eccezioni aggregate annidate

Se un'attività ha un'attività figlia associata che genera un'eccezione, tale eccezione viene incapsulata in un oggetto AggregateException prima che venga propagata all'attività padre, che incapsula tale eccezione nel proprio AggregateException prima di propagarla nuovamente al thread chiamante. In questi casi, la proprietà InnerExceptions dell'eccezione AggregateException intercettata nel metodo Task.Wait, WaitAny o WaitAll contiene una o più AggregateException istanze, non le eccezioni originali che hanno causato il problema. Per evitare di dover eseguire l'iterazione sulle eccezioni annidate AggregateException , è possibile usare il Flatten metodo per rimuovere tutte le eccezioni annidate AggregateException , in modo che la AggregateException.InnerExceptions proprietà contenga le eccezioni originali. Nell'esempio seguente, le istanze annidate AggregateException vengono appiattite e gestite in un solo ciclo.


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.

È possibile utilizzare anche il metodo AggregateException.Flatten per rilanciare le eccezioni interne dalle istanze generate da più attività AggregateException in una singola istanza AggregateException, come mostrato nell'esempio seguente.

public static partial class Program
{
    public static void TaskExceptionTwo()
    {
        try
        {
            ExecuteTasks();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                Console.WriteLine($"{e.GetType().Name}:\n   {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.

Eccezioni dalle attività figlio scollegate

Per impostazione predefinita, le attività figlio vengono create come scollegate. Le eccezioni generate dalle attività scollegate devono essere gestite o rigenerate nell'attività padre immediata; non vengono propagate nuovamente al thread chiamante nello stesso modo in cui le attività figlio collegate vengono propagate. Il genitore superiore può rilanciare manualmente un'eccezione da un figlio scollegato perché venga incapsulata in un AggregateException e propagata nuovamente al thread chiamante.


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.

Anche se si usa una continuazione per osservare un'eccezione in un'attività figlia, l'eccezione deve comunque essere osservata dall'attività padre.

Eccezioni che indicano l'annullamento cooperativo

Quando il codice utente in un'attività risponde a una richiesta di annullamento, la procedura corretta consiste nel generare un OperationCanceledException passando il token di annullamento su cui è stata comunicata la richiesta. Prima di tentare di propagare l'eccezione, l'istanza dell'attività confronta il token nell'eccezione con quello passato al momento della creazione. Se sono identiche, l'attività avvolge un TaskCanceledException nel AggregateException, e questo può essere visualizzato quando vengono esaminate le eccezioni interne. Tuttavia, se il thread chiamante non è in attesa dell'attività, questa specifica eccezione non verrà propagata. Per altre informazioni, vedere Annullamento attività .

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)

Uso del metodo handle per filtrare le eccezioni interne

È possibile usare il AggregateException.Handle metodo per filtrare le eccezioni che è possibile considerare come "gestite" senza usare altre logiche. Nel delegato utente fornito al metodo AggregateException.Handle(Func<Exception,Boolean>), è possibile esaminare il tipo di eccezione, la proprietà Message o qualsiasi altra informazione su di essa che consenta di determinare se è innocua. Qualsiasi eccezione per la quale il delegato restituisce false viene rigenerata in una nuova istanza di AggregateException immediatamente dopo che il metodo AggregateException.Handle è stato restituito.

L'esempio seguente è funzionalmente equivalente al primo esempio di questo argomento, che esamina ogni eccezione nella AggregateException.InnerExceptions raccolta. Invece, questo gestore di eccezioni chiama il metodo oggetto AggregateException.Handle per ogni eccezione e rilancia solo le eccezioni che non sono istanze di 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!

Di seguito è riportato un esempio più completo che usa il AggregateException.Handle metodo per fornire una gestione speciale per un'eccezione UnauthorizedAccessException durante l'enumerazione dei file.

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($"{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($"{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.

Osservazione delle eccezioni tramite la proprietà Task.Exception

Se un'attività viene completata nello stato TaskStatus.Faulted, è possibile esaminare la proprietà Exception per individuare quale specifica eccezione ha causato il guasto. Un buon modo per osservare la Exception proprietà consiste nell'usare una continuazione che viene eseguita solo se l'attività precedente ha errori, come illustrato nell'esempio seguente.


public static partial class Program
{
    public static void ExceptionPropagationTwo()
    {
        _ = Task.Run(
            () => throw new CustomException("task1 faulted."))
            .ContinueWith(_ =>
            {
                if (_.Exception?.InnerException is { } inner)
                {
                    Console.WriteLine($"{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.

In un'applicazione significativa, il delegato di continuazione potrebbe registrare informazioni dettagliate sull'eccezione e potrebbe anche generare nuove attività per riprendersi dall'eccezione. In caso di errori di un'attività, le espressioni seguenti generano l'eccezione:

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

Usare un'istruzione try-catch per gestire e osservare le eccezioni generate. In alternativa, osservare l'eccezione accedendo alla Task.Exception proprietà .

Importante

Non può essere intercettato AggregateException in modo esplicito quando si usano le seguenti espressioni:

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

Evento UnobservedTaskException

In alcuni scenari, ad esempio quando si ospitano plug-in non attendibili, le eccezioni non dannose potrebbero essere comuni e potrebbe essere troppo difficile osservarle manualmente. In questi casi, è possibile gestire l'evento TaskScheduler.UnobservedTaskException . L'istanza System.Threading.Tasks.UnobservedTaskExceptionEventArgs passata al gestore può essere usata per impedire la propagazione dell'eccezione non osservata al thread di join.

Vedere anche