이 코드에서 :
private async void button1_Click(object sender, EventArgs e) {
try {
await Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2());
}
catch (Exception ex) {
// Expect AggregateException, but got InvalidTimeZoneException
}
}
Task DoLongThingAsyncEx1() {
return Task.Run(() => { throw new InvalidTimeZoneException(); });
}
Task DoLongThingAsyncEx2() {
return Task.Run(() => { throw new InvalidOperation();});
}
기다리고 있던 작업 중 하나 이상에서 예외가 발생했기 때문에 WhenAll
를 만들고 던질 것으로 예상 했습니다 AggregateException
. 대신 작업 중 하나에서 발생한 단일 예외가 반환됩니다.
않습니다 WhenAll
항상을 만들 수 없습니다 AggregateException
?
답변
나는 정확히 어디에 있는지 기억하지 못하지만 새로운 async / await 키워드 AggregateException
를 사용하여 실제 예외로 풀린다는 것을 읽었습니다 .
따라서 catch 블록에서 집계 된 예외가 아닌 실제 예외가 발생합니다. 이를 통해보다 자연스럽고 직관적 인 코드를 작성할 수 있습니다.
이는 또한 많은 코드가 집계 된 예외가 아닌 특정 예외를 예상하는 경우 기존 코드를 async / await 사용으로 쉽게 변환하는 데 필요했습니다 .
— 편집하다 —
알았다:
Bill Wagner의 비동기 입문서
Bill Wagner는 다음과 같이 말했습니다 : ( 예외 발생시 )
… await를 사용하면 컴파일러에서 생성 된 코드가 AggregateException을 풀고 기본 예외를 throw합니다. await를 활용하면 Task.Result, Task.Wait 및 Task 클래스에 정의 된 기타 Wait 메서드에서 사용하는 AggregateException 유형을 처리하기위한 추가 작업을 피할 수 있습니다. 이것이 기본 Task 메서드 대신 await를 사용하는 또 다른 이유입니다 ….
답변
나는 이것이 이미 답변 된 질문이라는 것을 알고 있지만 선택한 답변은 실제로 OP의 문제를 해결 하지 못 하므로 이것을 게시 할 것이라고 생각했습니다.
이 솔루션은 집계 예외 (즉 , 다양한 작업에서 발생한 모든 예외)를 제공하고 차단하지 않습니다 (워크 플로는 여전히 비동기적임).
async Task Main()
{
var task = Task.WhenAll(A(), B());
try
{
var results = await task;
Console.WriteLine(results);
}
catch (Exception)
{
if (task.Exception != null)
{
throw task.Exception;
}
}
}
public async Task<int> A()
{
await Task.Delay(100);
throw new Exception("A");
}
public async Task<int> B()
{
await Task.Delay(100);
throw new Exception("B");
}
핵심은 집계 작업에 대한 참조를 대기하기 전에 저장 한 다음 AggregateException을 보유하는 Exception 속성에 액세스 할 수 있습니다 (단 하나의 작업에서만 예외가 발생하더라도).
이것이 여전히 유용하기를 바랍니다. 나는 오늘이 문제가 있다는 것을 알고 있습니다.
답변
모든 작업을 탐색하여 둘 이상의 작업에서 예외가 발생했는지 확인할 수 있습니다.
private async Task Example()
{
var tasks = new [] { DoLongThingAsyncEx1(), DoLongThingAsyncEx2() };
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
var exceptions = tasks.Where(t => t.Exception != null)
.Select(t => t.Exception);
}
}
private Task DoLongThingAsyncEx1()
{
return Task.Run(() => { throw new InvalidTimeZoneException(); });
}
private Task DoLongThingAsyncEx2()
{
return Task.Run(() => { throw new InvalidOperationException(); });
}
답변
@Richiban의 답변을 확장하여 작업에서 참조하여 catch 블록에서 AggregateException을 처리 할 수도 있다고 생각했습니다. 예 :
async Task Main()
{
var task = Task.WhenAll(A(), B());
try
{
var results = await task;
Console.WriteLine(results);
}
catch (Exception ex)
{
// This doesn't fire until both tasks
// are complete. I.e. so after 10 seconds
// as per the second delay
// The ex in this instance is the first
// exception thrown, i.e. "A".
var firstExceptionThrown = ex;
// This aggregate contains both "A" and "B".
var aggregateException = task.Exception;
}
}
public async Task<int> A()
{
await Task.Delay(100);
throw new Exception("A");
}
public async Task<int> B()
{
// Extra delay to make it clear that the await
// waits for all tasks to complete, including
// waiting for this exception.
await Task.Delay(10000);
throw new Exception("B");
}
답변
당신은 생각하고 있습니다 Task.WaitAll
-그것은 AggregateException
.
WhenAll은 발생하는 예외 목록의 첫 번째 예외를 throw합니다.
답변
여기에 많은 좋은 답변이 있지만 동일한 문제를 발견하고 몇 가지 조사를 수행했기 때문에 여전히 내 호언을 게시하고 싶습니다. 또는 아래의 TLDR 버전으로 건너 뛰십시오.
문제
에서 task
반환 된를 기다리면 여러 작업에 오류가 발생한 경우에도에 저장된에 Task.WhenAll
대한 첫 번째 예외 만 발생합니다.AggregateException
task.Exception
제공된 작업 중 하나라도 결함이있는 상태에서 완료되면 반환 된 작업도 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}");
}
답변
이것은 나를 위해 작동합니다
private async Task WhenAllWithExceptions(params Task[] tasks)
{
var result = await Task.WhenAll(tasks);
if (result.IsFaulted)
{
throw result.Exception;
}
}