Поймать исключение, вызванное асинхронным методом

Используя async ctp из Microsoft для .NET, возможно ли поймать исключение, вызванное асинхронным методом в вызывающем методе?

public async void Foo()
{
    var x = await DoSomethingAsync();
    /* Handle the result, but sometimes an exception might be thrown
       For example, DoSomethingAsync get 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
             when deployed the app will simply crash. */
    }
}

Итак, в основном я хочу, чтобы исключение из асинхронного кода появилось в моем кодовом коде если это вообще возможно вообще.

Ответ 1

Немного странно читать, но да, исключение будет зависеть от вызывающего кода - но он будет делать это только , если вы await или Wait() вызываете Foo

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

//or//

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

Обратите внимание, что использование Wait() может привести к блокировке вашего приложения, если .Net решит выполнить ваш метод синхронно.

Это объяснение http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions довольно хорошо - в нем обсуждаются шаги, которые компилятор предпринимает для достижения этой магии.

Ответ 2

Причина, по которой исключение не поймано, состоит в том, что метод Foo() имеет тип возврата void, и поэтому, когда ожидание вызвано, оно просто возвращается. Поскольку DoFoo() не ожидает завершения Foo, обработчик исключений не может быть использован.

Это открывает более простое решение, если вы можете изменить сигнатуры метода - alter 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
    }
}

Ответ 3

В вашем коде не так, как вы думаете. Асинхронные методы возвращаются сразу же после того, как метод начнет ждать результата 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   

Вы заметите, что метод Run завершен в потоке 2880, а только один расчет начался в другом потоке. Если вы поставили try/catch вокруг вашего метода ожидания, вы можете "поймать" исключение обычным способом, хотя ваш код выполняется в другом потоке, когда задача вычисления завершена, и ваше замедление выполняется.

Метод расчета автоматически отслеживает созданное исключение, потому что я использовал ApiChange.Api.dll из инструмента ApiChange. Tracing and Reflector помогает понять, что происходит. Чтобы избавиться от потоковой обработки, вы можете создавать свои собственные версии GetAwaiter BeginAwait и EndAwait и обернуть не задачу, но, например, Lazy и отслеживать внутри ваших собственных методов расширения. Тогда вы поймете, что лучше понять, что такое компилятор и что делает TPL.

Теперь вы видите, что нет способа получить попытку/уловить ваше исключение, так как для любого исключения не существует фрейма стека. После того, как вы инициировали операции async, ваш код мог бы сделать что-то совершенно другое. Он может вызвать Thread.Sleep или даже завершить. Пока есть одна передняя нить, ваше приложение с удовольствием продолжит выполнение асинхронных задач.


Вы можете обработать исключение внутри метода async после завершения асинхронной операции и перезвонить в поток пользовательского интерфейса. Рекомендуемый способ сделать это - TaskScheduler.FromSynchronizationContext. Это работает, только если у вас есть поток пользовательского интерфейса, и он не очень занят другими вещами.

С уважением, Алоис Краус

Ответ 4

Также важно отметить, что вы потеряете хронологическую трассировку стека исключения, если у вас есть тип возврата void для метода async. Я бы рекомендовал вернуть Task следующим образом. Переход к отладке намного проще.

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

Ответ 5

Исключение может быть обнаружено в асинхронной функции.

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get 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();
}

Ответ 6

В этом блоге подробно описывается ваша проблема Async Best Practices.

Суть его в том, что вы не должны использовать void в качестве возврата для метода async, если он не обработчик события async, это плохая практика, потому что он не позволяет исключить исключения; -).

Лучшей практикой было бы изменить тип возвращаемого значения на задачу. Кроме того, попробуйте код async all the way torough, сделайте каждый вызов метода async и вызывается из методов async. За исключением основного метода в консоли, который не может быть асинхронным.

Если вы проигнорируете эту передовую практику, вы столкнетесь с взаимоблокировками с приложениями с графическим интерфейсом и ASP.NET. Тупик возникает из-за того, что эти приложения работают в контексте, который позволяет использовать только один поток и не откажется от него в асинхронном потоке. Это означает, что GUI ждет синхронно для возврата, в то время как асинхронный метод ожидает контекста: deadlock.

Такое поведение не будет выполняться в консольном приложении, поскольку оно выполняется в контексте с пулом потоков. Метод async вернется в другой поток, который будет запланирован. Вот почему тестовое консольное приложение будет работать, но одни и те же вызовы будут заторможены в других приложениях...