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 task
tarafından döndürülen Task.WhenAll
sadece ilk durum atar AggregateException
saklanan task.Exception
birden 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.Exception
türdür AggregateException
ve await
devamlı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.Exception
yalnı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)
{
Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}
Burada, bizim sahip olabileceğimiz gibi , bir örneği AggregateException
ilk iç istisnasına InvalidOperationException
sarılır Task.WhenAll
. Biz gözlemlemek için başarısız olabilir DivideByZeroException
biz geçmesi olmasaydı task.Exception.InnerExceptions
direk.
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.InnerExceptions
ve 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)
{
Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));
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;
AggregateException
Bir veya daha fazla görev tarafından toplu olarak birden fazla istisna atılmışsa bir alın ;
Task
Sadece kontrol etmek için kaydetmek zorunda kalmayın Task.Exception
;
- Düzgün (iptal durumunu yaymak
Task.IsCanceled
bö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
{
public static Task WithAggregatedExceptions(this Task @this)
{
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");
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
. ÖrneğinizdeTask.Wait
yerine kullandıysanızawait
, yakalarsınızAggregateException