Bắt một ngoại lệ được ném bởi một phương thức void async


282

Sử dụng CTP async từ Microsoft cho .NET, có thể bắt ngoại lệ được ném bởi một phương thức async trong phương thức gọi không?

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. */
    }
}

Vì vậy, về cơ bản tôi muốn ngoại lệ từ mã async xuất hiện trong mã gọi của mình nếu điều đó thậm chí có thể xảy ra.


1
Điều này có giúp gì cho bạn không? social.msdn.microsoft.com/Forums/en/async/thread/...
svrist

22
Trong trường hợp bất kỳ ai vấp phải điều này trong tương lai, bài viết Async / Await Best Practice ... có một lời giải thích tốt về nó trong "Hình 2 trường hợp ngoại lệ từ phương pháp không đồng bộ Async không thể bị bắt". " Khi một ngoại lệ được đưa ra khỏi phương thức Nhiệm vụ async hoặc Nhiệm vụ không đồng bộ <T>, ngoại lệ đó được ghi lại và đặt vào đối tượng Nhiệm vụ. Với các phương thức void async, không có đối tượng Nhiệm vụ, bất kỳ ngoại lệ nào được đưa ra khỏi phương thức void async sẽ được nâng lên trực tiếp trên SyncizationContext đã hoạt động khi phương thức void async bắt đầu. "
Mr Moose

Bạn có thể sử dụng phương pháp này hoặc cách này
Tselofan

Câu trả lời:


262

Nó hơi lạ khi đọc nhưng đúng vậy, ngoại lệ sẽ nổi lên theo mã cuộc gọi - nhưng chỉ khi bạn awaithoặc Wait()cuộc gọi đếnFoo .

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. */
    }
} 

Các phương thức void Async có ngữ nghĩa xử lý lỗi khác nhau. Khi một ngoại lệ được đưa ra khỏi phương thức Tác vụ không đồng bộ hoặc Tác vụ không đồng bộ, ngoại lệ đó được ghi lại và đặt vào đối tượng Tác vụ. Với các phương thức void async, không có đối tượng Nhiệm vụ, do đó, bất kỳ trường hợp ngoại lệ nào được đưa ra khỏi phương thức void async sẽ được đưa ra trực tiếp trên SyncizationContext đã hoạt động khi phương thức void async bắt đầu. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Lưu ý rằng việc sử dụng Wait () có thể khiến ứng dụng của bạn bị chặn, nếu .Net quyết định thực hiện đồng bộ phương thức của bạn.

Giải thích này http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions là khá tốt - nó thảo luận về các bước mà trình biên dịch thực hiện để đạt được phép thuật này.


3
Tôi thực sự muốn nói rằng nó rất đơn giản để đọc - trong khi tôi biết những gì thực sự đang diễn ra rất phức tạp - vì vậy bộ não của tôi đang bảo tôi đừng tin vào mắt mình ...
Stuart

8
Tôi nghĩ rằng phương thức Foo () nên được đánh dấu là Nhiệm vụ thay vì void.
Sornii

4
Tôi khá chắc chắn rằng điều này sẽ tạo ra một AggregateException. Như vậy, khối bắt như xuất hiện trong câu trả lời này sẽ không bắt được ngoại lệ.
xanadont

2
"nhưng chỉ khi bạn chờ đợi hoặc Chờ () cuộc gọi đến Foo" Làm thế nào bạn có thể awaitgọi cho Foo, khi Foo trở về khoảng trống? async void Foo(). Type void is not awaitable?
nghĩa

3
Không thể chờ đợi phương pháp void, có thể?
Hitesh P

74

Lý do ngoại lệ không bị bắt là vì phương thức Foo () có kiểu trả về void và vì vậy khi chờ đợi được gọi, nó chỉ đơn giản trả về. Vì DoFoo () không chờ hoàn thành Foo, nên trình xử lý ngoại lệ không thể được sử dụng.

Điều này mở ra một giải pháp đơn giản hơn nếu bạn có thể thay đổi chữ ký phương thức - thay đổi Foo()để nó trả về kiểu Taskvà sau đó DoFoo()có thể await Foo(), như trong mã này:

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
Điều này thực sự có thể lén theo bạn và nên được trình biên dịch cảnh báo.
GGleGrand 18/03/2016

19

Mã của bạn không làm những gì bạn có thể nghĩ nó làm. Các phương thức async trở lại ngay lập tức sau khi phương thức bắt đầu chờ kết quả async. Thật sâu sắc khi sử dụng theo dõi để điều tra cách mã thực sự hoạt động.

Mã dưới đây thực hiện như sau:

  • Tạo 4 nhiệm vụ
  • Mỗi tác vụ sẽ tăng không đồng bộ một số và trả về số tăng
  • Khi kết quả không đồng bộ đã đến, nó được theo dõi.

 

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

Khi bạn quan sát dấu vế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   

Bạn sẽ nhận thấy rằng phương thức Run hoàn thành trên luồng 2820 trong khi chỉ có một luồng con hoàn thành (2756). Nếu bạn đặt thử / bắt xung quanh phương thức chờ đợi của mình, bạn có thể "bắt" ngoại lệ theo cách thông thường mặc dù mã của bạn được thực thi trên một luồng khác khi tác vụ tính toán kết thúc và việc thực hiện của bạn được thực hiện.

Phương pháp tính toán tự động theo dõi ngoại lệ bị ném vì tôi đã sử dụng ApiChange.Api.dll từ công cụ ApiChange . Truy tìm và phản xạ giúp rất nhiều để hiểu những gì đang xảy ra. Để thoát khỏi luồng, bạn có thể tạo các phiên bản GetAwaiter BeginAwait và EndAwait của riêng bạn và không phải là một nhiệm vụ mà là Lười và theo dõi bên trong các phương thức mở rộng của riêng bạn. Sau đó, bạn sẽ hiểu rõ hơn về trình biên dịch và TPL làm gì.

Bây giờ bạn thấy rằng không có cách nào để thử lại / bắt lại ngoại lệ của bạn vì không còn khung ngăn xếp nào cho bất kỳ ngoại lệ nào được truyền từ đó. Mã của bạn có thể đang làm một cái gì đó hoàn toàn khác sau khi bạn đã bắt đầu các hoạt động không đồng bộ. Nó có thể gọi Thread.S ngủ hoặc thậm chí chấm dứt. Miễn là còn một luồng tiền cảnh, ứng dụng của bạn sẽ vui vẻ tiếp tục thực thi các tác vụ không đồng bộ.


Bạn có thể xử lý ngoại lệ bên trong phương thức async sau khi thao tác không đồng bộ của bạn kết thúc và gọi lại vào luồng UI. Cách được đề xuất để thực hiện việc này là với TaskScheduler.FromSyn syncizationContext . Điều đó chỉ hoạt động nếu bạn có một giao diện người dùng và nó không quá bận rộn với những thứ khác.


5

Ngoại lệ có thể được bắt gặp trong chức năng 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
Này, tôi biết nhưng tôi thực sự cần thông tin đó trong DoFoo để tôi có thể hiển thị thông tin trong UI. Trong trường hợp này, giao diện người dùng rất quan trọng để hiển thị ngoại lệ vì nó không phải là công cụ người dùng cuối mà là công cụ để gỡ lỗi giao thức truyền thông
TimothyP 21/03

Trong trường hợp đó, các cuộc gọi lại có rất nhiều ý nghĩa. (Đại biểu async cũ tốt)
Sanjeevakumar Hiremath

@Tim: Bao gồm bất kỳ thông tin nào bạn cần trong ngoại lệ ném?
Eric J.

5

Cũng cần lưu ý rằng bạn sẽ mất dấu vết ngăn xếp theo thời gian của ngoại lệ nếu bạn có loại trả về khoảng trống trên phương thức async. Tôi khuyên bạn nên trả lại Nhiệm vụ như sau. Đi để làm cho việc gỡ lỗi dễ dàng hơn rất nhiều.

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

Điều này sẽ gây ra sự cố với không phải tất cả các đường dẫn trả về một giá trị, vì nếu có ngoại lệ thì không có giá trị nào được trả về, trong khi thử. returnTuy nhiên, nếu bạn không có câu lệnh nào , mã này sẽ hoạt động, vì Task"hoàn toàn" được trả về bằng cách sử dụng async / await.
Matias Grioni

2

Blog này giải thích vấn đề của bạn một cách gọn gàng Thực hành tốt nhất Async .

Ý chính của nó là bạn không nên sử dụng void làm trả về cho phương thức async, trừ khi đó là trình xử lý sự kiện không đồng bộ, đây là cách thực hành tồi vì nó không cho phép bắt ngoại lệ ;-).

Thực hành tốt nhất sẽ là thay đổi loại trả về thành Nhiệm vụ. Ngoài ra, hãy thử mã hóa async theo mọi cách, thực hiện mọi cuộc gọi phương thức async và được gọi từ các phương thức async. Ngoại trừ một phương thức Chính trong bảng điều khiển, không thể không đồng bộ (trước C # 7.1).

Bạn sẽ gặp phải bế tắc với các ứng dụng GUI và ASP.NET nếu bạn bỏ qua thực tiễn tốt nhất này. Sự bế tắc xảy ra bởi vì các ứng dụng này chạy trên một bối cảnh chỉ cho phép một luồng và sẽ không từ bỏ nó thành luồng không đồng bộ. Điều này có nghĩa là GUI chờ đồng bộ để trả về, trong khi phương thức async chờ ngữ cảnh: bế tắc.

Hành vi này sẽ không xảy ra trong một ứng dụng giao diện điều khiển, bởi vì nó chạy trên ngữ cảnh với nhóm luồng. Phương thức async sẽ trả về một luồng khác sẽ được lên lịch. Đây là lý do tại sao một ứng dụng bảng điều khiển thử nghiệm sẽ hoạt động, nhưng các cuộc gọi tương tự sẽ bế tắc trong các ứng dụng khác ...


1
"Ngoại trừ một phương thức chính trong bảng điều khiển, không thể không đồng bộ." Kể từ C # 7.1, giờ đây Main có thể là một liên kết
Adam
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.