Nhiều câu trả lời hay ở đây, nhưng tôi vẫn muốn đăng ý kiến của mình vì tôi vừa gặp vấn đề tương tự và tiến hành một số nghiên cứu. Hoặc bỏ qua phiên bản TLDR bên dưới.
Vấn đề
Đang chờ task
trả về bằng cách Task.WhenAll
chỉ ném ngoại lệ đầu tiên của AggregateException
lưu trữ trong task.Exception
, ngay cả khi nhiều tác vụ bị lỗi.
Các tài liệu hiện tại choTask.WhenAll
nói:
Nếu bất kỳ tác vụ nào được cung cấp hoàn thành ở trạng thái bị lỗi, tác vụ được trả lại cũng sẽ hoàn thành ở trạng thái Lỗi, trong đó các ngoại lệ của nó sẽ chứa tập hợp các ngoại lệ chưa được bao bọc từ mỗi tác vụ được cung cấp.
Điều đó đúng, nhưng nó không nói bất cứ điều gì về hành vi "mở gói" đã nói ở trên về thời điểm chờ đợi tác vụ trả về.
Tôi cho rằng, các tài liệu không đề cập đến nó vì hành vi đó không dành riêng choTask.WhenAll
.
Nó chỉ đơn giản Task.Exception
là loại AggregateException
và để await
liên tục, nó luôn được mở ra như là ngoại lệ bên trong đầu tiên của nó, theo thiết kế. Điều này rất tốt cho hầu hết các trường hợp, vì thường chỉ Task.Exception
bao gồm một ngoại lệ bên trong. Nhưng hãy xem xét mã này:
Task WhenAllWrong()
{
var tcs = new TaskCompletionSource<DBNull>();
tcs.TrySetException(new Exception[]
{
new InvalidOperationException(),
new DivideByZeroException()
});
return tcs.Task;
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// task.Exception is an AggregateException with 2 inner exception
Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));
// However, the exception that we caught here is
// the first exception from the above InnerExceptions list:
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}
Ở đây, một phiên bản của AggregateException
được mở ra khỏi ngoại lệ bên trong đầu tiên của nó InvalidOperationException
theo cách giống hệt như chúng ta có thể đã làm với nó Task.WhenAll
. Chúng tôi có thể không quan sát được DivideByZeroException
nếu chúng tôi không task.Exception.InnerExceptions
trực tiếp đi qua .
Stephen Toub của Microsoft giải thích lý do đằng sau hành vi này trong vấn đề GitHub liên quan :
Điểm mà tôi đang cố gắng đưa ra là nó đã được thảo luận sâu, nhiều năm trước, khi những điều này ban đầu được thêm vào. Ban đầu chúng tôi đã làm những gì bạn đề xuất, với Task được trả về từ WhenAll chứa một AggregateException duy nhất chứa tất cả các ngoại lệ, tức là task.Exception sẽ trả về một trình bao bọc AggregateException chứa một AggregateException khác sau đó chứa các ngoại lệ thực tế; sau đó khi nó được chờ đợi, AggregateException bên trong sẽ được truyền. Phản hồi mạnh mẽ mà chúng tôi nhận được khiến chúng tôi thay đổi thiết kế là a) phần lớn các trường hợp như vậy đều có những ngoại lệ khá đồng nhất, chẳng hạn như việc tuyên truyền tất cả trong một tổng thể không quan trọng lắm, b) tuyên truyền tổng hợp sau đó phá vỡ kỳ vọng xung quanh sản lượng khai thác đối với các loại ngoại lệ cụ thể, và c) đối với những trường hợp ai đó muốn tổng hợp, họ có thể làm như vậy một cách rõ ràng với hai dòng như tôi đã viết. Chúng tôi cũng đã có các cuộc thảo luận sâu rộng về hành vi của sự chờ đợi sẽ như thế nào đối với các nhiệm vụ có nhiều ngoại lệ và đây là nơi chúng tôi hạ cánh.
Một điều quan trọng khác cần lưu ý, hành vi mở gói này là nông cạn. Tức là, nó sẽ chỉ mở ngoại lệ đầu tiên khỏi AggregateException.InnerExceptions
và để nó ở đó, ngay cả khi nó là một thể hiện của ngoại lệ khác AggregateException
. Điều này có thể thêm một lớp nhầm lẫn khác. Ví dụ, hãy thay đổi WhenAllWrong
như thế này:
async Task WhenAllWrong()
{
await Task.FromException(new AggregateException(
new InvalidOperationException(),
new DivideByZeroException()));
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// now, task.Exception is an AggregateException with 1 inner exception,
// which is itself an instance of AggregateException
Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));
// And now the exception that we caught here is that inner AggregateException,
// which is also the same object we have thrown from WhenAllWrong:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
Một giải pháp (TLDR)
Vì vậy, trở lại await Task.WhenAll(...)
, những gì cá nhân tôi muốn là có thể:
- Nhận một ngoại lệ duy nhất nếu chỉ một ngoại lệ đã được ném;
- Nhận một
AggregateException
nếu nhiều hơn một ngoại lệ đã được ném chung bởi một hoặc nhiều nhiệm vụ;
- Tránh phải lưu
Task
duy nhất để kiểm tra nó Task.Exception
;
- Tuyên truyền các trạng thái hủy đúng cách (
Task.IsCanceled
), như một cái gì đó như thế này sẽ không làm điều đó: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.
Tôi đã tập hợp các tiện ích mở rộng sau cho điều đó:
public static class TaskExt
{
/// <summary>
/// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
/// </summary>
public static Task WithAggregatedExceptions(this Task @this)
{
// using AggregateException.Flatten as a bonus
return @this.ContinueWith(
continuationFunction: anteTask =>
anteTask.IsFaulted &&
anteTask.Exception is AggregateException ex &&
(ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
Task.FromException(ex.Flatten()) : anteTask,
cancellationToken: CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler: TaskScheduler.Default).Unwrap();
}
}
Bây giờ, những điều sau đây hoạt động theo cách tôi muốn:
try
{
await Task.WhenAll(
Task.FromException(new InvalidOperationException()),
Task.FromException(new DivideByZeroException()))
.WithAggregatedExceptions();
}
catch (OperationCanceledException)
{
Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
Trace.WriteLine("2 or more exceptions");
// Now the exception that we caught here is an AggregateException,
// with two inner exceptions:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
Trace.WriteLine($"Just a single exception: ${exception.Message}");
}
AggregateException
. Nếu bạn sử dụngTask.Wait
thay vìawait
trong ví dụ của bạn, bạn muốn bắtAggregateException