Gestione delle eccezioni (Task Parallel Library)
Salvo in determinati scenari descritti più avanti in questo argomento, le eccezioni non gestite generate da codice utente in esecuzione in un'attività vengono propagate nel thread di unione. Le eccezioni vengono propagate quando si usa uno dei metodi Task.Wait di istanza e li si gestisce includendo la chiamata in un'istruzione try
/catch
. Se un'attività è il padre di attività figlio connesse o se si è in attesa di più attività, potrebbero essere generate più eccezioni.
Per propagare tutte le eccezioni nel thread chiamante, l'infrastruttura di Task ne esegue il wrapping in un'istanza di AggregateException . L'eccezione AggregateException ha una proprietà InnerExceptions che può essere enumerata per esaminare tutte le eccezioni originali generate e per gestire (o non gestire) individualmente ognuna di esse. È anche possibile gestire le eccezioni originali usando il metodo AggregateException.Handle .
Anche se viene generata un'unica eccezione, il sistema ne esegue comunque il wrapping in un oggetto 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 rilevando solo l'oggetto AggregateException senza osservare alcuna eccezione interna. Tuttavia, è consigliabile evitare questo approccio perché è analogo al rilevamento del tipo Exception di base negli scenari senza parallelismo. Rilevare un'eccezione senza intraprendere azioni specifiche per gestirla può lasciare il programma in uno stato indeterminato.
Se non si vuole chiamare il metodo Task.Wait per attendere il completamento dell'attività, è anche possibile recuperare l'eccezione AggregateException dalla proprietà Exception dell'attività, come illustrato nell'esempio seguente. Per altre informazioni, vedere la sezione Osservazione delle eccezioni con la proprietà Task.Exception in questo argomento.
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 ciclo while
che esegue il polling della proprietà Task.IsCompleted dell'attività per determinare quando l'attività è stata completata. Questa operazione non deve mai essere eseguita nel codice di produzione perché è molto inefficiente.
Se non si resta in attesa di un'attività che propaga un'eccezione o se si accede alla relativa proprietà Exception , il sistema esegue l'escalation dell'eccezione in base ai criteri delle eccezioni di .NET quando l'attività viene raccolta nel Garbage Collector.
Quando alle eccezioni è consentita la propagazione fino al thread di unione, un'attività potrebbe continuare a elaborare alcuni elementi dopo la generazione dell'eccezione.
Nota
Quando "Just My Code" è abilitato, Visual Studio in alcuni casi si interromperà in corrispondenza della riga che genera l'eccezione e visualizzerà un messaggio di errore simile a "Eccezione non gestita dal codice utente". Questo errore non è grave. È possibile premere F5 per continuare e osservare il comportamento di gestione delle eccezioni illustrato in questi esempi. Per impedire l'interruzione di Visual Studio al primo errore, deselezionare semplicemente la casella di controllo Abilita Just My Code in Strumenti, Opzioni, Debug, Generale.
Attività figlio connesse e oggetti AggregateException annidati
Se un'attività presenta un'attività figlio connessa che genera un'eccezione, il sistema ne eseguirà il wrapping in un oggetto AggregateException prima che venga propagata nell'attività padre che a sua volta ne eseguirà il wrapping nel proprio oggetto AggregateException prima che venga ripropagata al thread chiamante. In casi di questo tipo, nella proprietà InnerExceptions dell'eccezione AggregateException rilevata nel metodo Task.Wait o WaitAny o WaitAll sono contenute una o più istanze di AggregateException, non le eccezioni originali che hanno provocato l'errore. Per evitare di dove eseguire l'iterazione di oggetti AggregateException annidati, è possibile usare il metodo Flatten per rimuovere tutti gli oggetti AggregateException annidati, in modo che nella proprietà AggregateException.InnerExceptions siano contenute le eccezioni originali. L'esempio seguente applica automaticamente il metodo Flatten alle istanze di AggregateException annidate e le gestisce 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.
È anche possibile usare il metodo AggregateException.Flatten per generare nuovamente le eccezioni interne da più istanze di AggregateException generate da più attività in una singola istanza di AggregateException, come illustrato 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(
"{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.
Eccezioni provenienti da attività figlio disconnesse
Per impostazione predefinita, le attività figlio vengono create come disconnesse. Le eccezioni generate dalle attività disconnesse devono essere gestite o generate nuovamente nell'attività padre immediata. A differenza di quanto accade per le attività figlio connesse, tali eccezioni non vengono propagate nel thread chiamante. Il padre di primo livello può rigenerare manualmente un'eccezione proveniente da un figlio disconnesso per fare in modo che ne venga eseguito il wrapping in un oggetto AggregateException e venga propagata nel thread di unione.
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à figlio, l'eccezione deve comunque essere osservata dall'attività padre.
Eccezioni che indicano un annullamento cooperativo
Quando il codice utente in un'attività risponde a una richiesta di annullamento, la procedura corretta è generare un oggetto OperationCanceledException che passa il token di annullamento usato per comunicare la richiesta. Prima di provare a propagare l'eccezione, l'istanza dell'attività confronta il token nell'eccezione con quello che le è stato passato quando è stata creata. Se sono uguali, l'attività propaga un oggetto TaskCanceledException con wrapping nell'oggetto AggregateExceptione che può essere visto quando vengono esaminate le eccezioni interne. Tuttavia, se il thread di unione non è in attesa dell'attività, questa eccezione specifica non verrà propagata. Per altre informazioni, vedere 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)
Uso del metodo Handle per filtrare le eccezioni interne
È possibile usare il metodo AggregateException.Handle per escludere le eccezioni che è possibile trattare come "gestite" senza dover usare altre risorse di logica. Nel delegato dell'utente fornito al metodo AggregateException.Handle(Func<Exception,Boolean>) è possibile esaminare il tipo di eccezione o la relativa proprietà Message o qualsiasi altra informazione relativa a essa che consentirà di determinare se è non dannosa. Qualsiasi eccezione per cui viene restituito false
dal delegato viene generata nuovamente in una nuova istanza di AggregateException subito dopo la restituzione di un valore da parte del metodo AggregateException.Handle.
L'esempio seguente è funzionalmente equivalente al primo esempio in questo argomento, che esamina ogni eccezione nella raccolta AggregateException.InnerExceptions. Al contrario, questo gestore di eccezioni chiama l'oggetto del metodo AggregateException.Handle per ogni eccezione e rigenera 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 metodo AggregateException.Handle per fornire una gestione speciale di 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(
"{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.
Osservazione delle eccezioni con la proprietà Task.Exception
Se un'attività viene completata con lo stato TaskStatus.Faulted , è possibile esaminarne la proprietà Exception per trovare l'eccezione specifica che ha provocato l'errore. Un modo efficiente per osservare la proprietà Exception consiste nell'usare una continuazione che viene eseguita solo se nell'attività precedente si verifica un errore, 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("{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.
In un'applicazione significativa, il delegato della continuazione potrebbe registrare informazioni dettagliate sull'eccezione ed eventualmente generare nuove attività per recuperare 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 proprietà Task.Exception.
Importante
Impossibile intercettare AggregateException in modo esplicito quando si usano le espressioni seguenti:
await task
task.GetAwaiter().GetResult()
Evento UnobservedTaskException
In alcuni scenari, ad esempio durante l'hosting di plug-in non attendibili, le eccezioni benigne potrebbero essere comuni e potrebbe risultare troppo difficile osservarle tutte manualmente. In questi casi è possibile gestire l'evento TaskScheduler.UnobservedTaskException . L'istanza di System.Threading.Tasks.UnobservedTaskExceptionEventArgs passata al gestore può essere usata per evitare la propagazione dell'eccezione non osservata al thread di unione.