ตรวจจับข้อยกเว้นที่ถูกโยนโดยวิธีการ void async


283

การใช้ async CTP จาก Microsoft สำหรับ. NET เป็นไปได้หรือไม่ที่จะรับข้อยกเว้นที่เกิดจากวิธี async ในวิธีการโทร?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

ดังนั้นโดยทั่วไปฉันต้องการข้อยกเว้นจากรหัส async เพื่อทำให้ฟองสบู่ขึ้นในรหัสโทรของฉันหากเป็นไปได้


1
สิ่งนี้ให้ความช่วยเหลือคุณหรือไม่? social.msdn.microsoft.com/Forums/en/async/thread/…
svrist

22
ในกรณีที่ทุกคนสะดุดกับเรื่องนี้ในอนาคตบทความAsync / Await Best Practices ...มีคำอธิบายที่ดีใน "รูปที่ 2 ข้อยกเว้นจากวิธี Async Void ที่จับไม่ได้" " เมื่อข้อยกเว้นถูกโยนออกมาจากวิธี async Task หรือ async Task <T> วิธีนั้นข้อยกเว้นนั้นจะถูกดักจับและวางบนวัตถุ Task ด้วยวิธีการ void แบบ async จะไม่มีวัตถุ Task ยกเว้นข้อยกเว้นใด ๆ ที่ถูกโยนออกจากวิธีการ async เป็นโมฆะ จะถูกยกโดยตรงบน SynchronizationContext ที่ใช้งานอยู่เมื่อวิธีการโมฆะ async เริ่มต้น "
Mr Moose

คุณสามารถใช้วิธีนี้หรือนี่
Tselofan

คำตอบ:


263

มันค่อนข้างแปลกที่จะอ่าน แต่ใช่ฟองยกเว้นพระทัยถึงรหัสโทร - แต่ถ้าคุณawaitหรือWait()Fooการเรียกร้องให้

public async Task Foo()
{
    var x = await DoSomethingAsync();
}

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

วิธีการ Async เป็นโมฆะมีความหมายที่แตกต่างกันในการจัดการข้อผิดพลาด เมื่อมีข้อยกเว้นถูกโยนออกจากงาน async หรือวิธี async Task ข้อยกเว้นนั้นจะถูกดักจับและวางไว้บนวัตถุภารกิจ ด้วยวิธีการโมฆะ async ไม่มีวัตถุงานดังนั้นข้อยกเว้นใด ๆ ที่ถูกโยนออกมาจากวิธีการ async เป็นโมฆะจะถูกยกขึ้นโดยตรงใน SynchronizationContext ที่ใช้งานอยู่เมื่อวิธีโมฆะ async เริ่มต้นขึ้น - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

โปรดทราบว่าการใช้ Wait () อาจทำให้แอปพลิเคชันของคุณบล็อกหาก. Net ตัดสินใจที่จะใช้วิธีการของคุณแบบซิงโครนัส

คำอธิบายนี้http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptionsค่อนข้างดี - กล่าวถึงขั้นตอนที่คอมไพเลอร์ใช้เพื่อให้ได้เวทมนตร์นี้


3
จริง ๆ แล้วฉันหมายถึงมันเป็นตรงไปตรงมาเพื่ออ่าน - ในขณะที่ฉันรู้ว่าสิ่งที่เกิดขึ้นจริงมีความซับซ้อนจริง ๆ - ดังนั้นสมองของฉันบอกฉันว่าไม่เชื่อสายตาของฉัน ...
Stuart

8
ฉันคิดว่าวิธี Foo () ควรถูกทำเครื่องหมายเป็น Task แทนที่จะเป็นโมฆะ
Sornii

4
ฉันค่อนข้างแน่ใจว่านี่จะสร้าง AggregateException ด้วยเหตุนี้ catch catch ดังที่ปรากฏในคำตอบนี้จะไม่ตรวจจับข้อยกเว้น
xanadont

2
"แต่ถ้าคุณรอหรือรอ () การโทรถึง Foo" คุณawaitจะโทรหา Foo ได้อย่างไรเมื่อ Foo กลับมาเป็นโมฆะ? async void Foo(). Type void is not awaitable?
rism

3
ไม่สามารถรอวิธีการโมฆะใช่หรือไม่
Hitesh P

74

เหตุผลที่ข้อยกเว้นไม่ถูกตรวจจับได้เนื่องจากเมธอด Foo () มีประเภทการคืนค่าเป็นโมฆะและเมื่อเรียกใช้งานการรอก็จะส่งกลับ เนื่องจาก DoFoo () ไม่รอการเสร็จสิ้นของ Foo จึงไม่สามารถใช้ตัวจัดการข้อยกเว้นได้

นี้เปิดขึ้นเป็นทางออกที่ง่ายถ้าคุณสามารถเปลี่ยนลายเซ็นวิธีการ - ปรับเปลี่ยนFoo()เพื่อให้มันกลับชนิดTaskแล้วDoFoo()สามารถawait Foo()เช่นเดียวกับในรหัสนี้

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}

19
สิ่งนี้สามารถแอบดูคุณและควรได้รับคำเตือนจากคอมไพเลอร์
GGleGrand

19

รหัสของคุณไม่ได้ทำในสิ่งที่คุณคิดว่าทำ เมธอด Async กลับมาทันทีหลังจากเมธอดเริ่มรอผลการซิงค์ การใช้การติดตามอย่างละเอียดเพื่อตรวจสอบว่ารหัสทำงานอย่างไร

รหัสด้านล่างทำสิ่งต่อไปนี้:

  • สร้าง 4 งาน
  • แต่ละงานจะเพิ่มตัวเลขแบบไม่พร้อมกันและคืนจำนวนที่เพิ่มขึ้น
  • เมื่อผลลัพธ์ของการซิงค์มาถึงแล้วจะมีการติดตาม

 

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

เมื่อคุณสังเกตเห็นร่องรอย

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

คุณจะสังเกตเห็นว่าวิธีการเรียกใช้เสร็จสมบูรณ์ในเธรด 2820 ในขณะที่เพียงหนึ่งเธรดลูกได้เสร็จสิ้นแล้ว (2756) หากคุณลอง / ตรวจสอบวิธีการรอของคุณคุณสามารถ "ตรวจจับ" ข้อยกเว้นในแบบปกติแม้ว่ารหัสของคุณจะถูกเรียกใช้งานในเธรดอื่นเมื่องานการคำนวณเสร็จสิ้น

วิธีการคำนวณติดตามข้อยกเว้นที่ส่งออกมาโดยอัตโนมัติเพราะฉันใช้ ApiChange.Api.dll จากเครื่องมือApiChange การสืบค้นกลับและตัวสะท้อนแสงช่วยให้หลาย ๆ คนเข้าใจสิ่งที่เกิดขึ้น ในการกำจัดเธรดคุณสามารถสร้าง GetAwaiter รุ่น BeginAwait และ EndAwait รุ่นของคุณเองและไม่ห่อหุ้มงาน แต่อย่างเช่น Lazy และ trace ภายในเมธอดส่วนขยายของคุณเอง จากนั้นคุณจะได้รับความเข้าใจที่ดีขึ้นว่าคอมไพเลอร์และสิ่งที่ TPL ทำ

ตอนนี้คุณเห็นว่าไม่มีทางที่จะลอง / จับข้อยกเว้นของคุณกลับมาได้เนื่องจากไม่มีกรอบสแต็กเหลือสำหรับข้อยกเว้นใด ๆ ที่จะเผยแพร่ รหัสของคุณอาจทำสิ่งที่แตกต่างอย่างสิ้นเชิงหลังจากที่คุณเริ่มต้นการดำเนินการ async มันอาจจะเรียก Thread.Sleep หรือแม้กระทั่งยุติ ตราบใดที่มีเธรดด้านหน้าเบื้องหน้าเหลือแอปพลิเคชันของคุณจะยังคงทำงานแบบอะซิงโครนัสอย่างต่อเนื่อง


คุณสามารถจัดการข้อยกเว้นภายในเมธอด async หลังจากการดำเนินการแบบอะซิงโครนัสของคุณเสร็จสิ้นและโทรกลับไปยังเธรด UI วิธีที่แนะนำให้ทำเช่นนี้คือมีTaskScheduler.FromSynchronizationContext ใช้งานได้เฉพาะเมื่อคุณมีเธรด UI และไม่ยุ่งกับสิ่งอื่น ๆ


5

ข้อยกเว้นสามารถติดอยู่ในฟังก์ชั่น async

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}

2
เฮ้ฉันรู้ แต่ฉันต้องการข้อมูลใน DoFoo จริงๆฉันจึงสามารถแสดงข้อมูลใน UI ได้ ในกรณีนี้มันเป็นสิ่งสำคัญสำหรับ UI ที่จะแสดงข้อยกเว้นเนื่องจากมันไม่ใช่เครื่องมือของผู้ใช้ แต่เป็นเครื่องมือในการดีบั๊กโปรโตคอลการสื่อสาร
TimothyP

ในกรณีดังกล่าวการโทรกลับมีความหมายมาก (ตัวแทน async เก่าที่ดี)
Sanjeevakumar Hiremath

@Tim: รวมข้อมูลอะไรก็ตามที่คุณต้องการในการยกเว้น
Eric J.

5

สิ่งสำคัญที่ควรทราบคือคุณจะสูญเสียการติดตามสแต็คตามลำดับเวลาของข้อยกเว้นหากคุณมีประเภทส่งคืนโมฆะในวิธีการแบบอะซิงก์ ฉันขอแนะนำให้ส่งคืนภารกิจดังต่อไปนี้ จะทำให้การดีบักง่ายขึ้นมากทั้ง

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

สิ่งนี้จะทำให้เกิดปัญหากับเส้นทางที่ไม่ได้ทั้งหมดส่งคืนค่าเนื่องจากหากมีข้อยกเว้นจะไม่มีการส่งคืนค่าในขณะที่ลองใช้ ถ้าคุณไม่มีreturnคำสั่งรหัสนี้ทำงาน แต่ตั้งแต่Taskเป็น "โดยปริยาย" async / awaitกลับโดยใช้
Matias Grioni

2

บล็อกนี้จะอธิบายถึงปัญหาของคุณได้อย่างเรียบร้อยAsync ปฏิบัติที่ดีที่สุด

ส่วนสำคัญของการเป็นคุณไม่ควรใช้โมฆะเป็นผลตอบแทนสำหรับวิธีการ async เว้นแต่ว่ามันเป็นตัวจัดการเหตุการณ์ async นี่คือการปฏิบัติที่ไม่ดีเพราะมันไม่อนุญาตให้จับข้อยกเว้น ;-)

วิธีปฏิบัติที่ดีที่สุดคือการเปลี่ยนประเภทผลตอบแทนเป็นงาน นอกจากนี้ให้ลองรหัส async ตลอดเส้นทางทำการโทรทุกวิธี async และถูกเรียกจากวิธีการ async ยกเว้นเมธอด Main ในคอนโซลซึ่งไม่สามารถเป็น async (ก่อน C # 7.1)

คุณจะพบกับ deadlocks ด้วยแอพพลิเคชั่น GUI และ ASP.NET หากคุณไม่สนใจแนวปฏิบัติที่ดีที่สุดนี้ การหยุดชะงักเกิดขึ้นเนื่องจากแอปพลิเคชันเหล่านี้ทำงานในบริบทที่อนุญาตเพียงหนึ่งเธรดและจะไม่ปล่อยไปที่เธรด async ซึ่งหมายความว่า GUI จะรอการส่งคืนพร้อมกันในขณะที่วิธีการซิงค์รอบริบท: การหยุดชะงัก

ลักษณะการทำงานนี้จะไม่เกิดขึ้นในแอปพลิเคชันคอนโซลเนื่องจากจะทำงานในบริบทด้วยเธรดพูล เมธอด async จะส่งคืนบนเธรดอื่นซึ่งจะถูกกำหนดเวลา นี่คือเหตุผลที่แอปคอนโซลทดสอบทำงาน แต่การโทรเดียวกันจะหยุดชะงักในแอปพลิเคชันอื่น ...


1
"ยกเว้นเมธอดหลักในคอนโซลซึ่งไม่สามารถเป็น async ได้" ตั้งแต่ C # 7.1, ตอนนี้ Main สามารถเป็นลิงค์
Adam
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.