ทำไมไม่รอ Task WhenAll โยน AggregateException?


106

ในรหัสนี้:

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?


8
WhenAll ไม่AggregateExceptionสร้าง ถ้าคุณใช้Task.Waitแทนawaitในตัวอย่างของคุณคุณจะจับAggregateException
Peter Ritchie

2
+1 นี่คือสิ่งที่ฉันพยายามหาช่วยฉันประหยัดเวลาในการดีบักและ google-ing
kennyzx

เป็นครั้งแรกในรอบสองสามปีที่ฉันต้องการข้อยกเว้นทั้งหมดTask.WhenAllและฉันก็ตกอยู่ในกับดักเดียวกัน ฉันได้ลองลงรายละเอียดลึก ๆเกี่ยวกับพฤติกรรมนี้แล้ว
noseratio

คำตอบ:


78

ฉันจำไม่ได้ว่าที่ไหน แต่ฉันอ่านที่ไหนสักแห่งที่มีคำหลักasync / awaitใหม่พวกเขาจะAggregateExceptionเปิดข้อยกเว้นจริง

ดังนั้นในการดักจับคุณจะได้รับข้อยกเว้นที่แท้จริงไม่ใช่ข้อยกเว้นที่รวบรวม สิ่งนี้ช่วยให้เราเขียนโค้ดที่เป็นธรรมชาติและใช้งานง่ายมากขึ้น

นอกจากนี้ยังจำเป็นสำหรับการแปลงโค้ดที่มีอยู่ให้ง่ายขึ้นโดยใช้async / awaitโดยที่โค้ดจำนวนมากคาดว่าจะมีข้อยกเว้นที่เฉพาะเจาะจงและไม่รวมข้อยกเว้น

- แก้ไข -

รับทราบ:

Async Primer โดย Bill Wagner

Bill Wagner กล่าวว่า: (ในเมื่อมีข้อยกเว้นเกิดขึ้น )

... เมื่อคุณใช้ await โค้ดที่สร้างโดยคอมไพลเลอร์จะคลาย AggregateException และแสดงข้อยกเว้นที่อยู่ภายใต้ ด้วยการใช้ประโยชน์จากการรอคุณจะหลีกเลี่ยงงานพิเศษในการจัดการประเภท AggregateException ที่ใช้โดย Task.Result, Task.Wait และวิธีการ Wait อื่น ๆ ที่กำหนดไว้ในคลาส Task นั่นเป็นอีกเหตุผลหนึ่งที่ต้องใช้การรอแทนวิธีการงานที่สำคัญ ....


4
ใช่ฉันรู้ว่ามีการเปลี่ยนแปลงบางอย่างในการจัดการข้อยกเว้น แต่เอกสารใหม่ล่าสุดสำหรับ Task whenAll state "ถ้างานที่ให้มาเสร็จสมบูรณ์ในสถานะที่ผิดพลาดงานที่ส่งคืนจะเสร็จสมบูรณ์ในสถานะ Faulted ซึ่งจะมีข้อยกเว้น การรวมชุดของข้อยกเว้นที่ไม่ได้ตัดออกจากแต่ละงานที่ให้มา ".... ในกรณีของฉันงานทั้งสองของฉันจะเสร็จสมบูรณ์ในสถานะที่ผิดพลาด ...
Michael Ray Lovett

4
@MichaelRayLovett: คุณไม่ได้เก็บงานที่ส่งคืนไว้ที่ใดก็ได้ ฉันเดิมพันเมื่อคุณดูคุณสมบัติ Exception ของงานนั้นคุณจะได้รับ AggregateException แต่ในรหัสของคุณคุณกำลังใช้การรอคอย นั่นทำให้ AggregateException ถูกคลายออกเป็นข้อยกเว้นจริง
decyclone

3
ฉันก็คิดเช่นนั้นเหมือนกัน แต่มีปัญหาสองประการเกิดขึ้น: 1) ดูเหมือนว่าฉันจะคิดไม่ออกว่าจะจัดเก็บงานอย่างไรดังนั้นฉันจึงสามารถตรวจสอบได้ (เช่น "Task myTask = await Task whenAll (... )" ไม่ ดูเหมือนจะไม่ได้ผลและ 2) ฉันเดาว่าฉันไม่เห็นว่าการรอคอยสามารถแสดงถึงข้อยกเว้นหลายข้อเป็นข้อยกเว้นเพียงข้อเดียวได้อย่างไร .. ควรรายงานข้อยกเว้นใด เลือกแบบสุ่ม?
Michael Ray Lovett

2
ใช่เมื่อฉันจัดเก็บงานและตรวจสอบใน try / catch ของ await ฉันเห็นว่ามีข้อยกเว้นคือ AggregatedException ดังนั้นเอกสารที่ฉันอ่านจึงถูกต้อง Task whenAll กำลังสรุปข้อยกเว้นใน AggregateException แต่แล้วการรอคอยก็กำลังแกะกล่องออก ตอนนี้ฉันกำลังอ่านบทความของคุณ แต่ฉันยังไม่เห็นว่าการรอคอยสามารถเลือกข้อยกเว้นเพียงข้อเดียวจาก AggregateExceptions และโยนข้อยกเว้นนั้นเทียบกับอีกข้อหนึ่งได้อย่างไร ..
Michael Ray Lovett

3
อ่านบทความขอบคุณ แต่ฉันยังไม่เข้าใจว่าทำไมการรอคอยจึงแสดงถึง AggregateException (แสดงถึงข้อยกเว้นหลายข้อ) เป็นเพียงข้อยกเว้นเดียว การจัดการข้อยกเว้นที่ครอบคลุมนั้นเป็นอย่างไร? .. ฉันเดาว่าฉันต้องการทราบว่างานใดที่มีข้อยกเว้นและงานใดที่พวกเขาโยนฉันจะต้องตรวจสอบวัตถุงานที่สร้างโดย Task whenAll ??
Michael Ray Lovett

62

ฉันรู้ว่านี่เป็นคำถามที่มีคำตอบแล้ว แต่คำตอบที่เลือกไม่ได้ช่วยแก้ปัญหาของ 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");
}

กุญแจสำคัญคือการบันทึกการอ้างอิงไปยังงานรวมก่อนที่คุณจะรอมันจากนั้นคุณสามารถเข้าถึงคุณสมบัติ Exception ซึ่งเก็บ AggregateException ของคุณไว้ (แม้ว่าจะมีเพียงงานเดียวที่มีข้อยกเว้น)

หวังว่านี่จะยังมีประโยชน์ ฉันรู้ว่าวันนี้ฉันมีปัญหานี้


คำตอบที่ชัดเจนยอดเยี่ยมนี่ควรเป็น IMO ที่เลือกไว้
bytedev

3
+1 แต่คุณใส่throw task.Exception;เข้าไปในcatchบล็อกไม่ได้หรือ? (มันทำให้ฉันสับสนที่เห็นการจับที่ว่างเปล่าเมื่อมีการจัดการข้อยกเว้นจริง ๆ )
AnorZaken

@AnorZaken แน่นอน; ฉันจำไม่ได้ว่าทำไมฉันถึงเขียนแบบนั้นในตอนแรก แต่ฉันไม่เห็นข้อเสียใด ๆ ดังนั้นฉันจึงย้ายมันไปไว้ในบล็อกจับ ขอบคุณ
Richiban

ข้อเสียเล็กน้อยของวิธีนี้คือสถานะการยกเลิก ( Task.IsCanceled) ไม่ได้รับการเผยแพร่อย่างถูกต้อง นี้สามารถแก้ได้โดยใช้ผู้ช่วยส่วนขยายเช่นนี้
noseratio

35

คุณสามารถสำรวจงานทั้งหมดเพื่อดูว่ามีข้อยกเว้นมากกว่าหนึ่งงานหรือไม่:

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
jenson-button-event

15
สองความคิดเห็นก่อนหน้านี้ไม่ถูกต้อง รหัสทำงานได้จริงและexceptionsมีทั้งข้อยกเว้นที่ถูกโยนทิ้ง
Tobias

DoLongThingAsyncEx2 () ต้องโยน InvalidOperationException () ใหม่แทน InvalidOperation () ใหม่
Artemious

9
เพื่อบรรเทาข้อสงสัยใด ๆ ที่นี่ผมใส่กันซอขยายที่หวังแสดงให้เห็นว่าวิธีการจัดการนี้เล่นออก: dotnetfiddle.net/X2AOvM คุณจะเห็นว่าawaitสาเหตุที่ทำให้ข้อยกเว้นแรกถูกคลายออก แต่ข้อยกเว้นทั้งหมดยังคงมีอยู่ในอาร์เรย์ของงาน
nuclearpidgeon

13

แค่คิดว่าฉันจะขยายคำตอบของ @ Richiban เพื่อบอกว่าคุณสามารถจัดการ AggregateException ในบล็อก catch ได้โดยอ้างอิงจากงาน เช่น:

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

12

คุณกำลังคิดถึงTask.WaitAll- มันพ่นAggregateExceptionไฟล์.

เมื่อ All เพียงแสดงข้อยกเว้นแรกของรายการข้อยกเว้นที่พบ


3
นี้เป็นธรรมงานกลับมาจากWhenAllวิธีการที่มีExceptionทรัพย์สินที่เป็นที่มีข้อยกเว้นทั้งหมดโยนในมันAggregateException InnerExceptionsสิ่งที่เกิดขึ้นที่นี่คือawaitการโยนข้อยกเว้นภายในตัวแรกแทนที่จะเป็นAggregateExceptionตัวมันเอง (เช่น decyclone กล่าว) การเรียกใช้Waitเมธอดของงานแทนที่จะรอจะทำให้ข้อยกเว้นเดิมถูกโยนทิ้งไป
ŞafakGür

อันที่จริงคำตอบนี้และผู้แสดงความคิดเห็นก่อนหน้านั้นถูกต้องทั้งคู่ awaitบนWhenAllจะแกะยกเว้นรวมและผ่านการยกเว้นครั้งแรกในรายการที่จะจับ เพื่อจุดประสงค์ของคำถามเดิมเพื่อให้ได้ข้อยกเว้นรวมในบล็อกจับตามที่คาดไว้Task.WaitAllควรใช้a
ธีโอ

9

คำตอบที่ดีมากมายที่นี่ แต่ฉันก็ยังอยากจะโพสต์คุยโวเพราะฉันเพิ่งเจอปัญหาเดียวกันและทำการวิจัย หรือข้ามไปยังเวอร์ชัน 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)
{
    // 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]);
}

นี่เป็นตัวอย่างของ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)
{
    // 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ข้อยกเว้นหากมีการโยนข้อยกเว้นมากกว่าหนึ่งรายการโดยงานตั้งแต่หนึ่งงานขึ้นไป
  • หลีกเลี่ยงการบันทึก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

จากนั้นเส้นโยนจะไม่ถูกตีที่นี่ - เมื่อไรที่ทุกคนจะโยนข้อยกเว้น
thab

-5

ในโค้ดของคุณข้อยกเว้นแรกจะถูกส่งกลับโดยการออกแบบตามที่อธิบายไว้ที่ http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/task-exception-handling-in-net-4-5 aspx

สำหรับคำถามของคุณคุณจะได้รับ AggreateException หากคุณเขียนโค้ดดังนี้:

try {
    var result = Task.WhenAll(DoLongThingAsyncEx1(), DoLongThingAsyncEx2()).Result; 
}
catch (Exception ex) {
    // Expect AggregateException here
} 
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.