여기에 많은 좋은 답변이 있지만 동일한 문제를 발견하고 몇 가지 조사를 수행했기 때문에 여전히 내 호언을 게시하고 싶습니다. 또는 아래의 TLDR 버전으로 건너 뛰십시오.
문제
에서 task
반환 된를 기다리면 여러 작업에 오류가 발생한 경우에도에 저장된에 Task.WhenAll
대한 첫 번째 예외 만 발생합니다.AggregateException
task.Exception
현재 문서Task.WhenAll
말 :
제공된 작업 중 하나라도 결함이있는 상태에서 완료되면 반환 된 작업도 Faulted 상태로 완료됩니다. 여기서 예외에는 제공된 각 작업에서 래핑되지 않은 예외 집합의 집계가 포함됩니다.
맞습니다. 그러나 반환 된 작업이 대기 할 때 앞서 언급 한 "언 래핑"동작에 대해서는 아무 것도 말하지 않습니다.
해당 동작이Task.WhenAll
.NET 전용이 아니기 때문에 문서에서 언급하지 않는 것 같습니다 .
그것은 단순히 Task.Exception
유형 AggregateException
이며 await
연속적인 경우 항상 설계 상 첫 번째 내부 예외로 풀립니다. 일반적 Task.Exception
으로 하나의 내부 예외로만 구성 되기 때문에 대부분의 경우에 좋습니다 . 그러나 다음 코드를 고려하십시오.
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]);
}
여기에서 AggregateException
get 의 인스턴스 는 .NET을 사용했을 InvalidOperationException
때와 똑같은 방식으로 첫 번째 내부 예외 로 풀 Task.WhenAll
립니다. 직접 DivideByZeroException
통과하지 않았다면 관찰 하지 못했을 task.Exception.InnerExceptions
수도 있습니다.
Microsoft의 Stephen Toub 는 관련 GitHub 문제 에서이 동작의 원인을 설명합니다 .
제가 말하고자했던 요점은 이것이 원래 추가되었을 때 몇 년 전 깊이 논의되었다는 것입니다. 원래는 모든 예외를 포함하는 단일 AggregateException을 포함하는 WhenAll에서 반환 된 Task를 사용하여 원래 제안한 작업을 수행했습니다. 즉, task.Exception은 실제 예외를 포함하는 다른 AggregateException을 포함하는 AggregateException 래퍼를 반환합니다. 그런 다음 기다릴 때 내부 AggregateException이 전파됩니다. 우리가 디자인을 변경하게 만든 강력한 피드백은 a) 그러한 사례의 대부분이 상당히 동질적인 예외를 가지고있어서 집합체로 모든 것을 전파하는 것이 그다지 중요하지 않았으며, b) 집합체를 전파 한 다음 어획물에 대한 기대치를 깨뜨렸다는 것입니다. 특정 예외 유형의 경우 c) 누군가가 집계를 원하는 경우 내가 쓴 두 줄로 명시 적으로 그렇게 할 수 있습니다. 또한 여러 예외가 포함 된 작업과 관련하여 await sould의 동작이 무엇인지에 대한 광범위한 토론을 진행했으며, 여기에 착수했습니다.
주목해야 할 또 다른 중요한 점은이 언 래핑 동작이 얕다는 것입니다. 즉, 다른 .NET의 AggregateException.InnerExceptions
인스턴스 인 경우에도 첫 번째 예외 만 풀고 그대로 둡니다 AggregateException
. 이것은 또 다른 혼란의 층을 추가 할 수 있습니다. 예를 들어 다음 WhenAllWrong
과 같이 변경해 보겠습니다 .
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));
}
솔루션 (TLDR)
다시로 돌아가서 await Task.WhenAll(...)
제가 개인적으로 원했던 것은 다음과 같은 기능을 제공하는 것입니다.
- 예외가 하나만 발생하면 단일 예외를 가져옵니다.
AggregateException
하나 이상의 작업에 의해 둘 이상의 예외가 집합 적으로 throw 된 경우 가져옵니다.
Task
확인 을 위해 저장하지 않아도됩니다 Task.Exception
.
- 취소 상태를 적절하게 전파하십시오 (
Task.IsCanceled
) Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.
이를 위해 다음 확장을 구성했습니다.
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();
}
}
이제 다음은 내가 원하는 방식으로 작동합니다.
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
. 예에서Task.Wait
대신 사용했다면await
AggregateException