Tại sao không chờ đợi Task.WhenAll ném AggregateException?


100

Trong mã này:

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

Tôi dự kiến ​​sẽ WhenAlltạo và ném một AggregateException, vì ít nhất một trong các nhiệm vụ mà nó đang chờ đã bị ném ra một ngoại lệ. Thay vào đó, tôi nhận lại một ngoại lệ duy nhất được ném bởi một trong các nhiệm vụ.

Không WhenAllphải lúc nào cũng tạo ra một AggregateException?


7
WhenAll không tạo ra một AggregateException. Nếu bạn sử dụng Task.Waitthay vì awaittrong ví dụ của bạn, bạn muốn bắtAggregateException
Peter Ritchie

2
+1, đây là điều tôi đang cố gắng tìm ra, giúp tôi tiết kiệm hàng giờ gỡ lỗi và nhập google.
kennyzx 23/10/12

Lần đầu tiên sau một vài năm, tôi cần tất cả các trường hợp ngoại lệ Task.WhenAll, và tôi đã rơi vào cái bẫy tương tự. Vì vậy, tôi đã cố gắng đi sâu vào chi tiết về hành vi này.
mũi

Câu trả lời:


75

Tôi không nhớ chính xác ở đâu, nhưng tôi đã đọc ở đâu đó rằng với các từ khóa async / await mới , chúng mở AggregateExceptionra trường hợp ngoại lệ thực sự.

Vì vậy, trong khối bắt, bạn nhận được ngoại lệ thực tế chứ không phải ngoại lệ tổng hợp. Điều này giúp chúng tôi viết mã tự nhiên và trực quan hơn.

Điều này cũng cần thiết để dễ dàng chuyển đổi mã hiện tại sang sử dụng async / await , trong đó rất nhiều mã mong đợi các ngoại lệ cụ thể chứ không phải ngoại lệ tổng hợp.

-- Biên tập --

Hiểu rồi:

Một lớp lót Async của Bill Wagner

Bill Wagner nói: (trong Khi ngoại lệ xảy ra )

... Khi bạn sử dụng await, mã do trình biên dịch tạo ra sẽ mở AggregateException và ném ngoại lệ cơ bản. Bằng cách tận dụng await, bạn tránh phải làm thêm công việc để xử lý kiểu AggregateException được sử dụng bởi Task.Result, Task.Wait và các phương thức Wait khác được định nghĩa trong lớp Task. Đó là một lý do khác để sử dụng await thay vì các phương thức Tác vụ cơ bản ....


3
Vâng, tôi biết có một số thay đổi đối với việc xử lý ngoại lệ, nhưng tài liệu mới nhất cho Task.WhenAll trạng thái "Nếu bất kỳ nhiệm 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 sự kết hợp của bộ ngoại lệ nào từ mỗi trong những nhiệm vụ cung cấp" .... trong trường hợp của tôi, cả hai nhiệm vụ của tôi được hoàn thành trong tình trạng faulted ...
Michael Ray Lovett

4
@MichaelRayLovett: Bạn không lưu trữ Nhiệm vụ đã trả lại ở bất kỳ đâu. Tôi cá rằng khi bạn nhìn vào thuộc tính Exception của tác vụ đó, bạn sẽ nhận được AggregateException. Tuy nhiên, trong mã của bạn, bạn đang sử dụng await. Điều đó làm cho AggregateException được bỏ vào ngoại lệ thực tế.
decyclone

3
Tôi cũng nghĩ đến điều đó, nhưng có hai vấn đề nảy sinh: 1) Tôi dường như không thể tìm ra cách lưu trữ tác vụ để tôi có thể kiểm tra nó (tức là "Task myTask = await Task.WhenAll (...)" doesn’t. dường như không hoạt động. và 2) Tôi đoán tôi không thấy làm thế nào mà await có thể đại diện cho nhiều trường hợp ngoại lệ chỉ là một trường hợp ngoại lệ .. trường hợp ngoại lệ nào nó nên báo cáo? Chọn ngẫu nhiên một cái?
Michael Ray Lovett

2
Có, khi tôi lưu trữ nhiệm vụ và kiểm tra nó trong lần thử / bắt của chờ đợi, tôi thấy ngoại lệ là AggregatedException. Vì vậy, các tài liệu tôi đọc là đúng; Task.WhenAll đang gói các ngoại lệ trong AggregateException. Nhưng sau đó chờ đợi đang mở chúng ra. Tôi đọc bài viết của bạn bây giờ, nhưng tôi không nhìn thấy nhưng bao nhiêu chờ đợi có thể chọn một ngoại lệ duy nhất từ AggregateExceptions và ném rằng một vs nhau ..
Michael Ray Lovett

2
Đọc bài viết, cảm ơn. Nhưng tôi vẫn không hiểu tại sao await đại diện cho một AggregateException (đại diện cho nhiều ngoại lệ) chỉ là một ngoại lệ duy nhất. Đó là cách xử lý toàn diện các trường hợp ngoại lệ? .. Tôi đoán nếu tôi muốn biết chính xác tác vụ nào đã ném ngoại lệ và tác vụ nào chúng đã ném, tôi sẽ phải kiểm tra đối tượng Tác vụ được tạo bởi Task.WhenAll ??
Michael Ray Lovett

55

Tôi biết đây là một câu hỏi đã được trả lời nhưng câu trả lời đã chọn không thực sự giải quyết được vấn đề của OP, vì vậy tôi nghĩ tôi sẽ đăng bài này.

Giải pháp này cung cấp cho bạn ngoại lệ tổng hợp (nghĩa là tất cả các ngoại lệ đã được ném bởi các tác vụ khác nhau) và không chặn (quy trình làm việc vẫn không đồng bộ).

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

Chìa khóa là lưu một tham chiếu đến tác vụ tổng hợp trước khi bạn đợi nó, sau đó bạn có thể truy cập thuộc tính Exception của nó, thuộc tính này chứa AggregateException của bạn (ngay cả khi chỉ có một tác vụ ném ra ngoại lệ).

Hy vọng điều này vẫn hữu ích. Tôi biết tôi đã gặp vấn đề này ngày hôm nay.


Câu trả lời rõ ràng xuất sắc, đây sẽ là IMO được chọn.
bytedev

3
+1, nhưng bạn không thể chỉ cần đặt khối throw task.Exception;bên trong catch? (Nó bối rối cho tôi để xem một bắt rỗng khi hợp ngoại lệ được thực sự đang được xử lý.)
AnorZaken

@AnorZaken Hoàn toàn có thể; Tôi không nhớ tại sao tôi lại viết nó như vậy ban đầu, nhưng tôi không thể thấy bất kỳ nhược điểm nào nên tôi đã di chuyển nó trong khối bắt. Cảm ơn
Richiban

Một nhược điểm nhỏ của phương pháp này là trạng thái hủy ( Task.IsCanceled) không được truyền bá đúng cách. Điều này có thể được giải quyết bằng cách sử dụng một trình trợ giúp mở rộng như thế này .
mũi

34

Bạn có thể duyệt qua tất cả các tác vụ để xem liệu có nhiều tác vụ đã đưa ra ngoại lệ hay không:

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
điều này không hoạt động. WhenAllthoát khỏi ngoại lệ đầu tiên và trả về điều đó. xem: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event 23/02/18

14
Hai nhận xét trước là không chính xác. Trên thực tế, mã này hoạt động và exceptionschứa cả hai trường hợp ngoại lệ.
Tobias

DoLongThingAsyncEx2 () phải ném InvalidOperationException mới () thay vì InvalidOperation mới ()
Artemious

8
Để giảm bớt bất kỳ nghi ngờ nào ở đây, tôi đã tập hợp một fiddle mở rộng hy vọng sẽ hiển thị chính xác cách xử lý này diễn ra: dotnetfiddle.net/X2AOvM . Bạn có thể thấy rằng các awaitnguyên nhân khiến ngoại lệ đầu tiên không được bao bọc, nhưng tất cả các ngoại lệ thực sự vẫn có sẵn thông qua mảng Nhiệm vụ.
phẫu thuật hạt nhân

13

Tôi chỉ nghĩ rằng tôi sẽ mở rộng câu trả lời của @ Richiban để nói rằng bạn cũng có thể xử lý AggregateException trong khối bắt bằng cách tham chiếu nó từ nhiệm vụ. Ví dụ:

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

Bạn đang nghĩ đến Task.WaitAll- nó ném một AggregateException.

WhenAll chỉ ném ngoại lệ đầu tiên trong danh sách các ngoại lệ mà nó gặp phải.


3
Điều này là sai, tác vụ được trả về từ WhenAllphương thức có một thuộc Exceptiontính là một AggregateExceptionchứa tất cả các ngoại lệ được đưa vào trong nó InnerExceptions. Điều đang xảy ra ở đây là awaitném ngoại lệ bên trong đầu tiên thay vì AggregateExceptionchính nó (như decyclone đã nói). Việc gọi Waitphương thức của nhiệm vụ thay vì chờ nó khiến ngoại lệ ban đầu được ném ra.
Şafak Gür

2

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ờ tasktrả về bằng cách Task.WhenAllchỉ ném ngoại lệ đầu tiên của AggregateExceptionlư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.Exceptionlà loại AggregateExceptionvà để awaitliê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.Exceptionbao 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ó InvalidOperationExceptiontheo 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 DivideByZeroExceptionnếu chúng tôi không task.Exception.InnerExceptionstrự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.InnerExceptionsvà để 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 WhenAllWrongnhư 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 AggregateExceptionnế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 Taskduy 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}");
}

1
Fantastic câu trả lời
cuộn

-3

Điều này phù hợp với tôi

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

1
WhenAllkhông giống như WhenAny. await Task.WhenAny(tasks)sẽ hoàn thành ngay khi hoàn thành bất kỳ nhiệm vụ nào. Vì vậy, nếu bạn có một nhiệm vụ hoàn thành ngay lập tức và thành công và một nhiệm vụ khác mất vài giây trước khi ném một ngoại lệ, tác vụ này sẽ trở lại ngay lập tức mà không có bất kỳ lỗi nào.
StriplingWarrior

Sau đó, dòng ném sẽ không bao giờ được nhấn ở đây - WhenAll sẽ ném ngoại lệ
thab

-4

Trong mã của bạn, ngoại lệ đầu tiên được trả về theo thiết kế như được giải thích tại http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5. aspx

Đối với câu hỏi của bạn, bạn sẽ nhận được AggreateException nếu bạn viết mã như thế này:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.