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 utilizza uno dei metodi Task.Wait o Task<TResult>.Wait statici o di istanza e si gestiscono tali metodi 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à, è possibile che vengano generate più eccezioni. Per propagare tutte le eccezioni nel thread chiamante, l'infrastruttura di Task esegue il wrapping di tali eccezioni in un'istanza di AggregateException. AggregateException presenta 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 se viene generata un'unica eccezione, il sistema ne esegue comunque il wrapping in un oggetto AggregateException.
Dim task1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("I'm bad, but not too bad!")
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
' Assume we know what's going on with this particular exception.
' Rethrow anything else. AggregateException.Handle provides
' another way to express this. See later example.
For Each ex In ae.InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
End Try
var task1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("I'm bad, but not too bad!");
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
// Assume we know what's going on with this particular exception.
// Rethrow anything else. AggregateException.Handle provides
// another way to express this. See later example.
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
}
È possibile evitare un'eccezione non gestita rilevando solo l'oggetto AggregateException senza osservare alcuna eccezione interna. Tuttavia, è consigliabile evitare questo approccio in quanto è 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 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, è possibile che un'attività continui a elaborare alcuni elementi dopo la generazione dell'eccezione.
Nota |
---|
Quando viene abilitato "Just My Code", in alcuni casi Visual Studio si interromperà in corrispondenza della riga che genera l'eccezione e in cui viene visualizzato un messaggio di errore che indica che l'eccezione non è gestita dal codice utente. Questo errore è benigno.È 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 semplicemente la casella di controllo "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 tali casi, la proprietà AggregateException().InnerExceptions di AggregateException rilevata nel metodo Task.Wait o Task<TResult>.Wait o WaitAny o WaitAll contiene una o più istanze di AggregateException, non le eccezioni originali che hanno provocato l'errore. Per evitare l'esigenza di scorrere oggetti AggregateExceptions annidati, è possibile utilizzare il metodo Flatten() per rimuovere tutti gli oggetti AggregateExceptions annidati, in modo che la proprietà AggregateException() InnerExceptions contenga le eccezioni originali. Nell'esempio seguente viene applicato automaticamente il metodo Flatten alle istanze di AggregateException annidate e le gestisce in un solo ciclo.
' task1 will throw an AE inside an AE inside an AE
Dim task1 = Task.Factory.StartNew(Sub()
Dim child1 = Task.Factory.StartNew(Sub()
Dim child2 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("Attached child2 faulted.")
End Sub,
TaskCreationOptions.AttachedToParent)
End Sub,
TaskCreationOptions.AttachedToParent)
' Uncomment this line to see the exception rethrown.
' throw new MyCustomException("Attached child1 faulted.")
End Sub)
Try
task1.Wait()
Catch ae As AggregateException
For Each ex In ae.Flatten().InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
'or like this:
' ae.Flatten().Handle(Function(e)
' Return TypeOf (e) Is MyCustomException
' End Function)
End Try
// task1 will throw an AE inside an AE inside an AE
var task1 = Task.Factory.StartNew(() =>
{
var child1 = Task.Factory.StartNew(() =>
{
var child2 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Attached child2 faulted.");
},
TaskCreationOptions.AttachedToParent);
// Uncomment this line to see the exception rethrown.
// throw new MyCustomException("Attached child1 faulted.");
},
TaskCreationOptions.AttachedToParent);
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
{
if (e is MyCustomException)
{
// Recover from the exception. Here we just
// print the message for demonstration purposes.
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
// or ...
// ae.Flatten().Handle((ex) => ex is MyCustomException);
}
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.
Dim task1 = Task.Factory.StartNew(Sub()
Dim nestedTask1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("Nested 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 MyCustomException Then
' Recover from the exception. Here we just
' print the message for demonstration purposes.
Console.WriteLine(ex.Message)
End If
Next
End Try
var task1 = Task.Factory.StartNew(() =>
{
var nested1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Nested task faulted.");
});
// Here the exception will be escalated back to joining thread.
// We could use try/catch here to prevent that.
nested1.Wait();
});
try
{
task1.Wait();
}
catch (AggregateException ae)
{
foreach (var e in ae.Flatten().InnerExceptions)
{
if (e is MyCustomException)
{
// Recover from the exception. Here we just
// print the message for demonstration purposes.
Console.WriteLine(e.Message);
}
}
}
Anche se si utilizza 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 utilizzato per comunicare la richiesta. Prima di tentare di 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 AggregateException e 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 ulteriori informazioni, vedere Annullamento delle attività.
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)
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task1 = Task.Factory.StartNew(() =>
{
CancellationToken ct = token;
while (someCondition)
{
// Do some work...
Thread.SpinWait(50000);
ct.ThrowIfCancellationRequested();
}
},
token);
// No waiting required.
Utilizzo del metodo Handle per filtrare le eccezioni interne
È possibile utilizzare il metodo Handle() per escludere le eccezioni che è possibile trattare come "gestite" senza dover utilizzare altre risorse di logica. Nel delegato dell'utente fornito a Handle() è possibile esaminare il tipo di eccezione o la relativa proprietà Message() o qualsiasi altra informazione relativa a essa che consentirà di determinare se è benigna. Qualsiasi eccezione per cui il delegato restituisce false viene generata nuovamente in una nuova istanza di AggregateException subito dopo il completamento dell'esecuzione di Handle().
Nel frammento seguente viene utilizzato un ciclo foreach sulle eccezioni interne.
For Each ex In ae.InnerExceptions
If TypeOf (ex) Is MyCustomException Then
Console.WriteLine(ex.Message)
Else
Throw
End If
Next
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
Il frammento seguente mostra l'utilizzo equivalente dal punto di vista funzionale del metodo Handle().
ae.Handle(Function(ex)
Return TypeOf (ex) Is MyCustomException
End Function)
ae.Handle((ex) =>
{
return ex is MyCustomException;
});
Osservazione delle eccezioni tramite la proprietà Task.Exception
Se un'attività viene completata con lo stato Faulted, è possibile esaminarne la proprietà Exception per individuare l'eccezione specifica che ha provocato l'errore. Un modo efficiente per osservare la proprietà Exception è utilizzare una continuazione che viene eseguita solo se nell'attività precedente si verifica un errore, come mostrato nell'esempio seguente.
Dim task1 = Task.Factory.StartNew(Sub()
Throw New MyCustomException("task1 faulted.")
End Sub).ContinueWith(Sub(t)
Console.WriteLine("I have observed a {0}", _
t.Exception.InnerException.GetType().Name)
End Sub,
TaskContinuationOptions.OnlyOnFaulted)
var task1 = Task.Factory.StartNew(() =>
{
throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
{
Console.WriteLine("I have observed a {0}",
t.Exception.InnerException.GetType().Name);
},
TaskContinuationOptions.OnlyOnFaulted);
In un'applicazione reale, il delegato della continuazione potrebbe registrare informazioni dettagliate sull'eccezione ed eventualmente generare nuove attività per gestire l'eccezione.
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 System.Threading.Tasks.UnobservedTaskExceptionEventArgs passata al gestore può essere utilizzata per evitare la propagazione dell'eccezione non osservata al thread di unione.