Task.WhenAll에서 AggregateException이 발생하는 이유는 무엇입니까?


102

이 코드에서 :

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?


7
WhenAll 않습니다 를 작성 AggregateException. 예에서 Task.Wait대신 사용했다면awaitAggregateException
Peter Ritchie

2
+1, 이것이 제가 알아 내고자하는 것입니다. 디버깅 및 Google-ing 시간을 절약 할 수 있습니다.
kennyzx

몇 년 만에 처음으로에서 모든 예외가 필요 Task.WhenAll했고 같은 함정에 빠졌습니다. 그래서 저는 이 동작에 대해 자세히 알아 보려고했습니다 .
noseratio

답변:


76

나는 정확히 어디에 있는지 기억하지 못하지만 새로운 async / await 키워드 AggregateException를 사용하여 실제 예외로 풀린다는 것을 읽었습니다 .

따라서 catch 블록에서 집계 된 예외가 아닌 실제 예외가 발생합니다. 이를 통해보다 자연스럽고 직관적 인 코드를 작성할 수 있습니다.

이는 또한 많은 코드가 집계 된 예외가 아닌 특정 예외를 예상하는 경우 기존 코드를 async / await 사용으로 쉽게 변환하는 데 필요했습니다 .

-- 편집하다 --

알았다:

Bill Wagner의 비동기 입문서

Bill Wagner는 다음과 같이 말했습니다 : ( 예외 발생시 )

... await를 사용하면 컴파일러에서 생성 된 코드가 AggregateException을 풀고 기본 예외를 throw합니다. await를 활용하면 Task.Result, Task.Wait 및 Task 클래스에 정의 된 기타 Wait 메서드에서 사용하는 AggregateException 유형을 처리하기위한 추가 작업을 피할 수 있습니다. 이것이 기본 Task 메서드 대신 await를 사용하는 또 다른 이유입니다 ....


3
예, 예외 처리에 몇 가지 변경 사항이 있다는 것을 알고 있지만 Task.WhenAll 상태에 대한 최신 문서 "제공된 작업 중 하나가 오류 상태에서 완료되면 반환 된 작업도 오류 상태에서 완료되며 예외가 포함됩니다. 내 경우에는 공급 작업 "의 각에서 풀어 예외 세트의 집계는 ..., 내 작업의 모두 ... 오류 상태에서 완료된다
마이클 레이 로베

4
@MichaelRayLovett : 반환 된 작업을 어디에도 저장하지 않습니다. 해당 작업의 Exception 속성을 볼 때 AggregateException이 발생합니다. 그러나 코드에서 await를 사용하고 있습니다. 그러면 AggregateException이 실제 예외로 언 래핑됩니다.
decyclone

3
나도 그렇게 생각했지만 두 가지 문제가 발생했습니다. 1) 작업을 저장하는 방법을 알아 내지 못해서 검사 할 수있는 것 같습니다 (예 : "Task myTask = await Task.WhenAll (...)"은 작동하지 않는 것 같습니다. 그리고 2) await가 어떻게 여러 예외를 하나의 예외로 나타낼 수 있는지 모르겠습니다. 어떤 예외를보고해야합니까? 무작위로 선택 하시겠습니까?
Michael Ray Lovett

2
예, 작업을 저장하고 대기의 try / catch에서 검사 할 때 예외가 AggregatedException이라는 것을 알 수 있습니다. 그래서 제가 읽은 문서는 옳습니다. Task.WhenAll은 AggregateException에서 예외를 래핑합니다. 그러나 await는 그들을 풀고 있습니다. 나는 지금 당신의 기사를 읽고 있지만, 어떻게 await가 AggregateExceptions에서 하나의 예외를 선택하고 하나 대 다른 예외를 던질 수 있는지 아직 알지 못합니다.
Michael Ray Lovett

3
기사를 읽어 주셔서 감사합니다. 하지만 왜 await가 AggregateException (여러 예외를 나타내는)을 하나의 단일 예외로 나타내는 지 이해하지 못합니다. 어떻게 포괄적 인 예외 처리입니까? .. 어떤 작업이 예외를 던졌고 어떤 작업이 던 졌는지 정확히 알고 싶다면 Task.WhenAll ??에 의해 생성 된 Task 객체를 조사해야 할 것 같습니다.
Michael Ray Lovett

55

나는 이것이 이미 답변 된 질문이라는 것을 알고 있지만 선택한 답변은 실제로 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 속성에 액세스 할 수 있습니다 (단 하나의 작업에서만 예외가 발생하더라도).

이것이 여전히 유용하기를 바랍니다. 나는 오늘이 문제가 있다는 것을 알고 있습니다.


아주 명확한 대답, 이것은 IMO가 선택되어야합니다.
bytedev

3
+1하지만 단순히 블록 throw task.Exception;안에 넣을 수는 catch없습니까? (예외가 실제로 처리되고있을 때 빈 catch를 보는 것이 혼란
스럽습니다

@AnorZaken 절대적으로; 원래 그렇게 썼던 이유는 기억 나지 않지만 단점이 보이지 않아 캐치 블록으로 이동했습니다. 감사합니다
Richiban

이 방법의 한 가지 작은 단점은 취소 상태 ( Task.IsCanceled)가 제대로 전파되지 않는다는 것입니다. 이 같은 확장 헬퍼 사용하여 해결할 수있는 문제가 될 수 .
noseratio

34

모든 작업을 탐색하여 둘 이상의 작업에서 예외가 발생했는지 확인할 수 있습니다.

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(); });
}

2
이것은 작동하지 않습니다. WhenAll첫 번째 예외가 발생하면 종료하고이를 반환합니다. 참조 : stackoverflow.com/questions/6123406/waitall-vs-whenall
젠슨 버튼 이벤트

14
앞의 두 주석이 올바르지 않습니다. 이 코드는 실제로 작동하며 exceptionsthrow 된 두 예외를 모두 포함합니다.
Tobias

DoLongThingAsyncEx2 ()는 new InvalidOperation () 대신 new InvalidOperationException ()을
던져야합니다

8
여기서 의문을 완화하기 위해이 처리 방식을 정확히 보여주는 확장 된 바이올린을 모았 습니다 : dotnetfiddle.net/X2AOvM . 당신은이 것을 볼 수 있습니다 await풀어해야 할 첫 번째 예외가 발생하지만 모든 예외는 작업의 배열을 통해 여전히 실제로 사용할 수 있습니다.
nuclearpidgeon

13

@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");
}

11

당신은 생각하고 있습니다 Task.WaitAll-그것은 AggregateException.

WhenAll은 발생하는 예외 목록의 첫 번째 예외를 throw합니다.


3
이것은 잘못된 것입니다. WhenAll메서드 에서 반환 된 작업 에는에서 throw 된 모든 예외를 포함 하는 Exception속성이 있습니다. 여기서 일어나는 일은 자체 대신에 첫 번째 내부 예외 를 던지는 것입니다 (디 사이클론이 말한 것처럼). 작업 을 기다리는 대신 작업의 메서드를 호출하면 원래 예외가 throw됩니다. AggregateExceptionInnerExceptionsawaitAggregateExceptionWait
Şafak Gür

3

여기에 많은 좋은 답변이 있지만 동일한 문제를 발견하고 몇 가지 조사를 수행했기 때문에 여전히 내 호언을 게시하고 싶습니다. 또는 아래의 TLDR 버전으로 건너 뛰십시오.

문제

에서 task반환 된를 기다리면 여러 작업에 오류가 발생한 경우에도에 저장된에 Task.WhenAll대한 첫 번째 예외 만 발생합니다.AggregateExceptiontask.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]);
}

여기에서 AggregateExceptionget 의 인스턴스 는 .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}");
}

2
환상적인 답변
롤백

-3

이것은 나를 위해 작동합니다

private async Task WhenAllWithExceptions(params Task[] tasks)
{
    var result = await Task.WhenAll(tasks);
    if (result.IsFaulted)
    {
                throw result.Exception;
    }
}

1
WhenAll과 같지 않습니다 WhenAny. await Task.WhenAny(tasks)작업이 완료되는 즉시 완료됩니다. 따라서 즉시 완료되고 성공한 작업이 있고 다른 작업은 예외를 발생시키기까지 몇 초가 걸리면 오류없이 즉시 반환됩니다.
StriplingWarrior

그런 다음 스로우 라인은 여기에서 절대 히트하지 않습니다-WhenAll이 예외를
던졌을

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.