คำตอบที่ดีมากมายที่นี่ แต่ฉันก็ยังอยากจะโพสต์คุยโวเพราะฉันเพิ่งเจอปัญหาเดียวกันและทำการวิจัย หรือข้ามไปยังเวอร์ชัน TLDRด้านล่าง
ปัญหา
รอการtask
ส่งคืนโดยTask.WhenAll
โยนข้อยกเว้นแรกของที่AggregateException
จัดเก็บไว้task.Exception
เท่านั้นแม้ว่าจะมีข้อผิดพลาดหลายงานก็ตาม
เอกสารปัจจุบันTask.WhenAll
พูด:
หากงานใด ๆ ที่ให้มาเสร็จสมบูรณ์ในสถานะที่ผิดพลาดงานที่ส่งคืนจะเสร็จสมบูรณ์ในสถานะที่ผิดพลาดเช่นกันซึ่งข้อยกเว้นจะมีการรวมชุดของข้อยกเว้นที่ไม่ได้แยกออกจากแต่ละงานที่ให้มา
ซึ่งถูกต้อง แต่ไม่ได้บอกอะไรเกี่ยวกับพฤติกรรม "การแกะ" ดังกล่าวข้างต้นเมื่อรองานส่งคืน
ฉันคิดว่าเอกสารไม่พูดถึงมันเพราะพฤติกรรมที่ไม่เฉพาะTask.WhenAll
มันเป็นเพียง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)
{
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]);
}
นี่เป็นตัวอย่างของAggregateException
ได้รับของขวัญที่จะยกเว้นภายในครั้งแรกในตรงทางเดียวกับที่เราอาจจะมีมันด้วยInvalidOperationException
Task.WhenAll
เราอาจจะล้มเหลวในการสังเกตDivideByZeroException
หากเราไม่ผ่านtask.Exception.InnerExceptions
โดยตรง
Stephen Toub จาก Microsoft อธิบายเหตุผลเบื้องหลังพฤติกรรมนี้ในปัญหา GitHub ที่เกี่ยวข้อง :
ประเด็นที่ฉันพยายามทำคือมีการพูดคุยในเชิงลึกเมื่อหลายปีก่อนเมื่อสิ่งเหล่านี้ถูกเพิ่มเข้ามา เดิมเราทำในสิ่งที่คุณแนะนำโดยงานที่ส่งคืนจาก WhenAll ที่มี AggregateException เดียวที่มีข้อยกเว้นทั้งหมดนั่นคืองานข้อยกเว้นจะส่งคืน AggregateException wrapper ซึ่งมี AggregateException อื่นซึ่งจะมีข้อยกเว้นจริง จากนั้นเมื่อรอคอย AggregateException ภายในจะถูกเผยแพร่ ข้อเสนอแนะที่ดีที่เราได้รับซึ่งทำให้เราเปลี่ยนการออกแบบคือก) กรณีดังกล่าวส่วนใหญ่มีข้อยกเว้นที่ค่อนข้างเป็นเนื้อเดียวกันดังนั้นการเผยแพร่ข้อมูลทั้งหมดในภาพรวมจึงไม่สำคัญ b) การเผยแพร่ผลรวมจากนั้นก็ทำลายความคาดหวังในการจับ สำหรับประเภทข้อยกเว้นเฉพาะ และ c) สำหรับกรณีที่มีคนต้องการการรวมก็สามารถทำได้อย่างชัดเจนด้วยสองบรรทัดเหมือนที่ฉันเขียน นอกจากนี้เรายังมีการอภิปรายอย่างกว้างขวางเกี่ยวกับพฤติกรรมของการรอคอยที่เกี่ยวข้องกับงานที่มีข้อยกเว้นหลายประการและนี่คือจุดที่เรามาถึง
สิ่งสำคัญอีกประการหนึ่งที่ควรทราบคือพฤติกรรมการคลายตัวนี้เป็นเรื่องที่ตื้นเขิน กล่าวคือจะแกะเฉพาะข้อยกเว้นแรก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)
{
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));
}
โซลูชัน (TLDR)
ดังนั้นกลับไปawait Task.WhenAll(...)
ที่สิ่งที่ฉันต้องการเป็นการส่วนตัวคือสามารถ:
- รับข้อยกเว้นเพียงครั้งเดียวหากมีการโยนเพียงครั้งเดียว
- รับ
AggregateException
ข้อยกเว้นหากมีการโยนข้อยกเว้นมากกว่าหนึ่งรายการโดยงานตั้งแต่หนึ่งงานขึ้นไป
- หลีกเลี่ยงการบันทึก
Task
เพียงสำหรับการตรวจสอบของTask.Exception
;
- เผยแพร่สถานะการยกเลิกต้อง (
Task.IsCanceled
) Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
เช่นบางอย่างเช่นนี้จะไม่ทำอย่างนั้น:
ฉันได้รวบรวมส่วนขยายต่อไปนี้ไว้ด้วยกัน:
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();
}
}
ตอนนี้สิ่งต่อไปนี้ใช้งานได้ตามที่ฉันต้องการ:
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
สร้าง ถ้าคุณใช้Task.Wait
แทนawait
ในตัวอย่างของคุณคุณจะจับAggregateException