Neden Task.WhenAll bir AggregateException oluşturmayı beklemiyor?


105

Bu kodda:

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

Beklediği görevlerden en az biri bir istisna WhenAlloluşturduğu için bir oluşturmayı ve atmayı AggregateExceptionbekliyordum. Bunun yerine, görevlerden biri tarafından atılan tek bir istisnayı geri alıyorum.

Her WhenAllzaman bir yaratmaz mı AggregateException?


8
WhenAll gelmez bir oluşturmak AggregateException. Örneğinizde Task.Waityerine kullandıysanız await, yakalarsınızAggregateException
Peter Ritchie

2
+1, anlamaya çalıştığım şey bu, beni saatlerce hata ayıklama ve google-ing'den kurtar.
kennyzx

Birkaç yıldır ilk defa tüm istisnalara ihtiyacım vardı Task.WhenAllve aynı tuzağa düştüm. Bu yüzden bu davranış hakkında derin ayrıntılara girmeyi denedim .
noseratio

Yanıtlar:


78

Nerede olduğunu tam olarak hatırlamıyorum, ama bir yerde yeni async / await anahtar kelimelerle, bunların AggregateExceptiongerçek istisnaya dönüştüğünü okudum .

Yani, catch bloğunda, toplu olanı değil gerçek istisnayı alırsınız. Bu, daha doğal ve sezgisel kod yazmamıza yardımcı olur.

Bu ayrıca , çok sayıda kodun toplu istisnalar değil belirli istisnalar beklediği durumlarda mevcut kodun zaman uyumsuz / bekleme kullanımına daha kolay dönüştürülmesi için gerekliydi .

-- Düzenle --

Anladım:

Bill Wagner'den Async Primer

Bill Wagner şunları söyledi: ( İstisnalar Olduğunda )

... await kullandığınızda, derleyici tarafından oluşturulan kod AggregateException öğesini açar ve temeldeki özel durumu atar. Await'ten yararlanarak, Task.Result, Task.Wait ve Task sınıfında tanımlanan diğer Wait yöntemleri tarafından kullanılan AggregateException türünü işlemek için fazladan çalışmadan kaçınırsınız. Bu, temeldeki Görev yöntemleri yerine await kullanmanın başka bir nedeni ...


3
Evet, istisna işlemede bazı değişiklikler olduğunu biliyorum, ancak Task.WhenAll durumu için en yeni belgeler "Sağlanan görevlerden herhangi biri hatalı bir durumda tamamlanırsa, döndürülen görev de istisnalarının içereceği Hatalı bir durumda tamamlanacaktır. sağlanan görevlerin her birinden paketlenmemiş istisnalar kümesinin toplanması ".... Benim durumumda, her iki görevim de hatalı bir durumda tamamlanıyor ...
Michael Ray Lovett

4
@MichaelRayLovett: Döndürülen Görevi hiçbir yerde depolamıyorsunuz. Bahse girerim o görevin Exception özelliğine baktığınızda bir AggregateException alacaksınız. Ancak kodunuzda await kullanıyorsunuz. Bu, AggregateException'ın gerçek istisnaya açılmasını sağlar.
decyclone

3
Bunu ben de düşündüm, ancak iki sorun ortaya çıktı: 1) Görevi inceleyebilmek için görevi nasıl saklayacağımı çözemiyorum (yani "Task myTask = await Task.WhenAll (...)" İşe yaramıyor gibi görünüyor ve 2) Beklentinin birden çok istisnayı tek bir istisna olarak nasıl temsil edebileceğini sanmıyorum .. Hangi istisnayı bildirmeli? Rastgele birini seçmek?
Michael Ray Lovett

2
Evet, görevi depoladığımda ve onu beklemenin dene / yakalamasında incelediğimde, bunun istisnasının AggregatedException olduğunu görüyorum. Yani okuduğum dokümanlar doğru; Task.WhenAll bir AggregateException özel durumlarını sarıyor. Ama beklemek onları paketinden çıkarmaktır. Şu anda makalenizi okuyorum, ancak henüz Toplam İstisnalar'dan tek bir istisnayı nasıl seçip bunu başka birine nasıl atabileceğinizi bilmiyorum ..
Michael Ray Lovett

3
Makaleyi okuyun, teşekkürler. Ancak await'in neden bir AggregateException'ı (birden çok istisnayı temsil eden) tek bir istisna olarak temsil ettiğini hala anlamıyorum. Bu, istisnaların kapsamlı bir şekilde ele alınması nasıl olur? .. Sanırım hangi görevlerin istisna attığını ve hangilerini attığını tam olarak bilmek istersem Task.WhenAll tarafından oluşturulan Task nesnesini incelemem gerekir.
Michael Ray Lovett

59

Bunun zaten cevaplanmış bir soru olduğunu biliyorum, ancak seçilen cevap OP'nin problemini gerçekten çözmüyor, bu yüzden bunu göndereceğimi düşündüm.

Bu çözüm size toplam istisnayı verir (yani çeşitli görevler tarafından atılan tüm istisnalar) ve engellemez (iş akışı hala zaman uyumsuzdur).

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

Anahtar, siz onu beklemeden önce birleştirilmiş göreve bir başvuru kaydetmektir, ardından AggregateException'ınızı tutan Exception özelliğine erişebilirsiniz (yalnızca bir görev bir istisna atsa bile).

Umarım bu hala yararlıdır. Bugün bu problemi yaşadığımı biliyorum.


Mükemmel net cevap, bu IMO seçilmelidir.
bytedev

3
+1, ancak bloğun throw task.Exception;içini catchkoyamaz mısın? (İstisnalar gerçekten ele alındığında boş bir yakalama görmek
kafamı karıştırıyor

@AnorZaken Kesinlikle; Orijinal olarak neden böyle yazdığımı hatırlamıyorum, ama herhangi bir olumsuz yanı göremiyorum, bu yüzden onu yakalama bloğuna taşıdım. Teşekkürler
Richiban

Bu yaklaşımın küçük bir dezavantajı, iptal durumunun ( Task.IsCanceled) düzgün şekilde yayılmamasıdır. Bu gibi bir uzantı yardımcı kullanarak çözer olabilir bu .
noseratio

35

Birden fazla kişinin bir istisna atıp atmadığını görmek için tüm görevler arasında geçiş yapabilirsiniz:

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
bu çalışmıyor. WhenAllilk istisnadan çıkar ve onu döndürür. bkz: stackoverflow.com/questions/6123406/waitall-vs-whenall
jenson-button-event

15
Önceki iki yorum yanlış. Kod aslında çalışır ve exceptionsatılan her iki istisnayı da içerir.
Tobias

DoLongThingAsyncEx2 () yeni InvalidOperationException (atmak gerekir) yerine yeni InvalidOperation ()
Artemious

9
Buradaki herhangi bir şüpheyi gidermek için, umarım bu işlemenin tam olarak nasıl işlediğini gösteren genişletilmiş bir keman oluşturdum: dotnetfiddle.net/X2AOvM . awaitİlk istisnanın açılmamasına neden olan nedenlerin olduğunu görebilirsiniz , ancak tüm istisnalar gerçekten Görevler dizisi aracılığıyla hala kullanılabilir.
nüklearpidgeon

13

@ Richiban'ın cevabını, görevden referans alarak catch bloğundaki AggregateException'ı da işleyebileceğinizi söylemek için genişletmeyi düşündüm. Örneğin:

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

Düşünüyorsunuz Task.WaitAll- bir AggregateException.

WhenAll, karşılaştığı istisnalar listesinin ilk istisnasını atar.


3
Bu yanlıştır, WhenAllyöntemden döndürülen görev, içinde atılan tüm özel durumları içeren bir Exceptionözelliğe sahiptir . Burada olan şey , kendisi yerine ilk iç istisnayı atmaktır (decyclone'un söylediği gibi). Görevin yöntemini beklemek yerine çağırmak, orijinal istisnanın atılmasına neden olur. AggregateExceptionInnerExceptionsawaitAggregateExceptionWait
Şafak Gür

5

Burada birçok iyi cevap var, ancak yine de aynı problemle karşılaştığım ve biraz araştırma yaptığım için rantımı göndermek istiyorum. Veya aşağıdaki TLDR sürümüne atlayın.

Sorun

Oluşmasını bekliyoruz tasktarafından döndürülen Task.WhenAllsadece ilk durum atar AggregateExceptionsaklanan task.Exceptionbirden fazla görevi hatalı olması bile,.

Şu anki dokümanlarTask.WhenAll :

Sağlanan görevlerden herhangi biri hatalı bir durumda tamamlanırsa, döndürülen görev de Hatalı durumda tamamlanır; burada istisnalar, sağlanan görevlerin her birinden sarılmamış istisnalar kümesinin toplamını içerecektir.

Bu doğru, ancak geri gönderilen görev beklendiğinde yukarıda bahsedilen "sarmalanma" davranışı hakkında hiçbir şey söylemiyor.

Sanırım, doktorlar bundan bahsetmiyor çünkü bu davranışa özgü değilTask.WhenAll .

Bu basitçe bir Task.Exceptiontürdür AggregateExceptionve awaitdevamlılıklar için tasarım gereği her zaman ilk iç istisnası olarak açılır. Bu çoğu durumda harikadır çünkü genellikle Task.Exceptionyalnızca bir iç istisnadan oluşur. Ancak bu kodu göz önünde bulundurun:

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

Burada, bizim sahip olabileceğimiz gibi , bir örneği AggregateExceptionilk iç istisnasına InvalidOperationExceptionsarılır Task.WhenAll. Biz gözlemlemek için başarısız olabilir DivideByZeroExceptionbiz geçmesi olmasaydı task.Exception.InnerExceptionsdirek.

Microsoft'tan Stephen Toub , ilgili GitHub sorununda bu davranışın arkasındaki nedeni açıklıyor :

Bahsetmeye çalıştığım nokta, yıllar önce bunlar ilk eklendiğinde derinlemesine tartışılmıştı. Başlangıçta önerdiğiniz şeyi yaptık, tüm istisnaları içeren tek bir AggregateException içeren WhenAll'dan döndürülen Task, yani task.Exception, daha sonra gerçek istisnaları içeren başka bir AggregateException içeren bir AggregateException sarmalayıcısı döndürürdü; daha sonra beklendiğinde, iç AggregateException yayılır. Tasarımı değiştirmemize neden olan güçlü geri bildirim şuydu: a) bu tür vakaların büyük çoğunluğunun oldukça homojen istisnaları vardı, öyle ki hepsini bir bütün halinde yaymak o kadar da önemli değildi, b) toplamı yaymak, ardından yakalamalarla ilgili beklentileri kırdı. belirli istisna türleri için, ve c) Birinin toplamı istediği durumlarda, bunu benim yazdığım gibi iki satırla açıkça yapabilirlerdi. Ayrıca, birden fazla istisna içeren görevlerle ilgili olarak bekleme davranışının ne olması gerektiği konusunda kapsamlı tartışmalar yaptık ve buraya iniş yaptık.

Unutulmaması gereken bir diğer önemli nokta, bu sarmalama davranışı sığdır. Yani, yalnızca ilk istisnayı açacak AggregateException.InnerExceptionsve başka birinin örneği olsa bile onu orada bırakacaktır AggregateException. Bu, başka bir kafa karışıklığı katmanı ekleyebilir. Örneğin, şöyle değiştirelim 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));
}

Bir çözüm (TLDR)

Öyleyse, await Task.WhenAll(...)kişisel olarak istediğim şey, şunları yapabilmekti:

  • Yalnızca bir tane atılmışsa tek bir istisna elde edin;
  • AggregateExceptionBir veya daha fazla görev tarafından toplu olarak birden fazla istisna atılmışsa bir alın ;
  • TaskSadece kontrol etmek için kaydetmek zorunda kalmayın Task.Exception;
  • Düzgün (iptal durumunu yaymak Task.IsCanceledböyle bir şey yapmazdım gibi): Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.

Bunun için aşağıdaki uzantıyı bir araya getirdim:

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

Şimdi, şu benim istediğim şekilde çalışıyor:

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
Harika cevap
yuvarlanır

-3

Bu benim için çalışıyor

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

1
WhenAllile aynı değil WhenAny. await Task.WhenAny(tasks)herhangi bir görev tamamlanır tamamlanmaz tamamlanacaktır. Bu nedenle, hemen tamamlanan ve başarılı olan bir göreviniz varsa ve bir başkası bir istisna atmadan önce birkaç saniye sürüyorsa, bu, herhangi bir hata olmadan hemen geri dönecektir.
StriplingWarrior

Sonra atış çizgisinden burada vurmak asla - özel durum olurdu WhenAll
thab

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.