使用Task Parallel Library Task.WhenAny()时如何处理异常
How to handle exceptions when using Task Parallel Library Task.WhenAny()
当我使用 Task.WhenAll()
函数并且任务中抛出异常时,抛出新的 AggregateException,我可以捕获它以查看任务中发生的所有异常。但是,当我使用 Task.WhenAny()
时,不会抛出任何异常。相反,我必须检查 Task.Exception
属性 的值以查看是否发生异常。这似乎是一种糟糕的代码味道,因为我必须记得在每次使用 Task.WhenAny()
时检查 Task.Exception
属性。难道不应该有更好的方法吗?
这是我的意思的一个例子:
private async void btnMultipleExceptions_Click(object sender, EventArgs e) {
var task1 = ThrowNotImplementedException();
var task2 = ThrowDivideByZeroException();
try {
Task task = await Task.WhenAny(task1, task2);
// Even if an exception is thrown in one of the tasks (in our case,
// task1 will throw first) no exception is thrown from
// the above await Task.WhenAny(). Instead, the exception is placed on the
// Task.Exception property. So I need to check for it every time
// I call Task.WhenAny()?
if (task.Exception != null) {
Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
task.Exception.InnerExceptions.Select(x => x.Message).ToArray()));
} else {
Console.WriteLine("No Exceptions!");
}
} catch(Exception ex) {
// Try to catch all exceptions???
AggregateException allEx = ex as AggregateException;
if (allEx != null) {
Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
allEx.InnerExceptions.Select(x => x.Message).ToArray()));
} else {
Console.WriteLine("Exceptions: " + ex.Message);
}
}
}
private async Task ThrowNotImplementedException() {
await Task.Delay(TimeSpan.FromSeconds(1));
throw new NotImplementedException();
}
private async Task ThrowDivideByZeroException() {
await Task.Delay(TimeSpan.FromSeconds(2));
throw new DivideByZeroException();
}
只是 await
从 WhenAny
返回的任务。如果有错误,它会打开异常并抛出它,如果没有,你知道它已经完成了,所以你可以继续。
或者,您可以简单地在 Task.WhenAny
上调用 Unwrap
,然后 await
。这在语义上与前面的选项相同;唯一的区别在于您是否认为这或多或少很清楚。
如果您发现自己经常解包 WhenAny
的结果,您可以简单地编写自己的实现来为您解包任务,并改用它们:
public static Task WhenAny(IEnumerable<Task> tasks)
{
return Task.WhenAny(tasks).Unwrap();
}
public static Task<T> WhenAny<T>(IEnumerable<Task<T>> tasks)
{
return Task.WhenAny(tasks).Unwrap();
}
//TODO add wrappers for the `params` overloads if you want them too
虽然已接受的答案会给您例外来自最先完成的任务。第二个任务中的异常仍将被抛出,并且在 .NET 4.5 之前的版本中,如果它未被观察到,它将在获得 GC 时在线程级别升级。观察和记录非致命任务异常是 Continuations 的一个很好的用途(我从你的 WhenAny()
场景中假设,如果你的一个任务失败,这是一个非致命的情况)。考虑这样的事情:
private static void LogIfErrors(Task source)
{
if(source.Exception == null) return;
source.Exception.Handle(ex =>
{
Log.Error("#unhandled #task #error", ex);
return true;
});
return;
}
private void DoStuff()
{
// note that you cannot inline the ContinueWith() statement,
// because it would change the value of task1 to hold your
// continuation instead of your parent task
var task1 = ThrowNotImplementedException();
task1.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);
var task2 = ThrowDivideByZeroException();
task2.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);
var firstCompleted = await Tasks.WhenAny(task1, task2).Unwrap();
}
现在即使你的任务很长-运行并且在你的第一个完成的任务被WhenAny()
返回很久之后抛出异常,你仍然有机会观察异常并(在 <= .NET 4.0 中)防止它杀死连接的线程。
等待等待
await await Task.WhenAny(...);
当我使用 Task.WhenAll()
函数并且任务中抛出异常时,抛出新的 AggregateException,我可以捕获它以查看任务中发生的所有异常。但是,当我使用 Task.WhenAny()
时,不会抛出任何异常。相反,我必须检查 Task.Exception
属性 的值以查看是否发生异常。这似乎是一种糟糕的代码味道,因为我必须记得在每次使用 Task.WhenAny()
时检查 Task.Exception
属性。难道不应该有更好的方法吗?
这是我的意思的一个例子:
private async void btnMultipleExceptions_Click(object sender, EventArgs e) {
var task1 = ThrowNotImplementedException();
var task2 = ThrowDivideByZeroException();
try {
Task task = await Task.WhenAny(task1, task2);
// Even if an exception is thrown in one of the tasks (in our case,
// task1 will throw first) no exception is thrown from
// the above await Task.WhenAny(). Instead, the exception is placed on the
// Task.Exception property. So I need to check for it every time
// I call Task.WhenAny()?
if (task.Exception != null) {
Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
task.Exception.InnerExceptions.Select(x => x.Message).ToArray()));
} else {
Console.WriteLine("No Exceptions!");
}
} catch(Exception ex) {
// Try to catch all exceptions???
AggregateException allEx = ex as AggregateException;
if (allEx != null) {
Console.WriteLine("Exceptions: " + string.Join(Environment.NewLine,
allEx.InnerExceptions.Select(x => x.Message).ToArray()));
} else {
Console.WriteLine("Exceptions: " + ex.Message);
}
}
}
private async Task ThrowNotImplementedException() {
await Task.Delay(TimeSpan.FromSeconds(1));
throw new NotImplementedException();
}
private async Task ThrowDivideByZeroException() {
await Task.Delay(TimeSpan.FromSeconds(2));
throw new DivideByZeroException();
}
只是 await
从 WhenAny
返回的任务。如果有错误,它会打开异常并抛出它,如果没有,你知道它已经完成了,所以你可以继续。
或者,您可以简单地在 Task.WhenAny
上调用 Unwrap
,然后 await
。这在语义上与前面的选项相同;唯一的区别在于您是否认为这或多或少很清楚。
如果您发现自己经常解包 WhenAny
的结果,您可以简单地编写自己的实现来为您解包任务,并改用它们:
public static Task WhenAny(IEnumerable<Task> tasks)
{
return Task.WhenAny(tasks).Unwrap();
}
public static Task<T> WhenAny<T>(IEnumerable<Task<T>> tasks)
{
return Task.WhenAny(tasks).Unwrap();
}
//TODO add wrappers for the `params` overloads if you want them too
虽然已接受的答案会给您例外来自最先完成的任务。第二个任务中的异常仍将被抛出,并且在 .NET 4.5 之前的版本中,如果它未被观察到,它将在获得 GC 时在线程级别升级。观察和记录非致命任务异常是 Continuations 的一个很好的用途(我从你的 WhenAny()
场景中假设,如果你的一个任务失败,这是一个非致命的情况)。考虑这样的事情:
private static void LogIfErrors(Task source)
{
if(source.Exception == null) return;
source.Exception.Handle(ex =>
{
Log.Error("#unhandled #task #error", ex);
return true;
});
return;
}
private void DoStuff()
{
// note that you cannot inline the ContinueWith() statement,
// because it would change the value of task1 to hold your
// continuation instead of your parent task
var task1 = ThrowNotImplementedException();
task1.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);
var task2 = ThrowDivideByZeroException();
task2.ContinueWith(LogIfErrors, TaskContinuationOptions.OnlyOnFaulted);
var firstCompleted = await Tasks.WhenAny(task1, task2).Unwrap();
}
现在即使你的任务很长-运行并且在你的第一个完成的任务被WhenAny()
返回很久之后抛出异常,你仍然有机会观察异常并(在 <= .NET 4.0 中)防止它杀死连接的线程。
等待等待
await await Task.WhenAny(...);