Captura una excepción lanzada por un método vacío asíncrono

283

Usando el CTP asíncrono de Microsoft para .NET, ¿es posible detectar una excepción lanzada por un método asíncrono en el método de llamada?

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

Así que, básicamente, quiero que la excepción del código asíncrono aparezca en mi código de llamada si eso es posible.

TimothyP
fuente
1
¿Esto te da alguna ayuda? social.msdn.microsoft.com/Forums/en/async/thread/…
svrist
22
En caso de que alguien tropiece con esto en el futuro, el artículo Async / Await Best Practices ... tiene una buena explicación en "Figura 2 Las excepciones de un método de vacío asíncrono no se pueden atrapar con Catch". " Cuando se arroja una excepción de una Tarea asíncrona o un método de Tarea asíncrona <T>, esa excepción se captura y se coloca en el objeto Tarea. Con los métodos vacíos asíncronos, no hay ningún objeto Tarea, ninguna excepción se expulsa de un método vacío asíncrono aparecerá directamente en el SynchronizationContext que estaba activo cuando comenzó el método de vacío asíncrono " .
Sr. Moose
Puede usar este enfoque o este
Tselofan

Respuestas:

263

Es algo extraño de leer, pero sí, la excepción aparecerá en el código de llamada, pero solo si usted awaito Wait()la llamada lo hacenFoo .

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

Los métodos de vacío asíncrono tienen diferentes semánticas de manejo de errores. Cuando se arroja una excepción de una Tarea asincrónica o un método de Tarea asincrónica, esa excepción se captura y se coloca en el objeto Tarea. Con los métodos de vacío asíncrono, no hay ningún objeto de tarea, por lo que las excepciones generadas por un método de vacío asíncrono se generarán directamente en el SynchronizationContext que estaba activo cuando se inició el método de vacío asíncrono. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Tenga en cuenta que el uso de Wait () puede hacer que su aplicación se bloquee, si .Net decide ejecutar su método sincrónicamente.

Esta explicación http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions es bastante buena: analiza los pasos que toma el compilador para lograr esta magia.

Stuart
fuente
3
De hecho, me refiero a que es recta hacia adelante para leer - mientras que yo sé lo que está pasando en realidad es muy complicado - por lo que mi cerebro me está diciendo que no creer lo que veía ...
Stuart
8
Creo que el método Foo () debería estar marcado como Tarea en lugar de nulo.
Sornii
44
Estoy bastante seguro de que esto producirá una AggregateException. Como tal, el bloque catch como aparece en esta respuesta no detectará la excepción.
xanadont
2
"pero solo si espera o espera () la llamada a Foo" ¿Cómo puede awaitllamar a Foo cuando Foo regresa vacío? async void Foo(). Type void is not awaitable?
rism
3
No puedo esperar el método vacío, ¿verdad?
Hitesh P
74

La razón por la que no se detecta la excepción es porque el método Foo () tiene un tipo de retorno nulo y, por lo tanto, cuando se llama a wait, simplemente regresa. Como DoFoo () no espera la finalización de Foo, no se puede usar el controlador de excepciones.

Esto abre una solución más simple si puede cambiar las firmas del método: modifique Foo()para que devuelva el tipo Tasky luego DoFoo()pueda await Foo(), como en este código:

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

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}
Rob Church
fuente
19
Esto realmente puede acercarse sigilosamente y debe ser advertido por el compilador.
GGleGrand
19

Su código no hace lo que podría pensar que hace. Los métodos asíncronos regresan inmediatamente después de que el método comienza a esperar el resultado asíncrono. Es útil utilizar el rastreo para investigar cómo se comporta realmente el código.

El siguiente código hace lo siguiente:

  • Crea 4 tareas
  • Cada tarea incrementará asíncronamente un número y devolverá el número incrementado
  • Cuando el resultado asíncrono ha llegado, se rastrea.

 

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

Cuando observas las huellas

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   

Notará que el método Run se completa en el hilo 2820 mientras que solo un hilo hijo ha terminado (2756). Si pone un método try / catch alrededor de su método de espera, puede "capturar" la excepción de la manera habitual, aunque su código se ejecuta en otro hilo cuando la tarea de cálculo ha finalizado y se ejecuta su continuación.

El método de cálculo rastrea la excepción lanzada automáticamente porque utilicé ApiChange.Api.dll de la herramienta ApiChange . Tracing and Reflector ayuda mucho a comprender lo que está sucediendo. Para deshacerse de los subprocesos, puede crear sus propias versiones de GetAwaiter BeginAwait y EndAwait y ajustar no una tarea sino, por ejemplo, un Lazy y rastrear dentro de sus propios métodos de extensión. Entonces comprenderá mucho mejor lo que hace el compilador y lo que hace el TPL.

Ahora verá que no hay forma de volver a intentar / atrapar su excepción ya que no queda ningún marco de pila para que se propague cualquier excepción. Su código podría estar haciendo algo totalmente diferente después de haber iniciado las operaciones asíncronas. Puede llamar a Thread.Sleep o incluso terminar. Mientras quede un subproceso en primer plano, su aplicación seguirá felizmente ejecutando tareas asincrónicas.


Puede manejar la excepción dentro del método asincrónico después de que su operación asincrónica haya finalizado y volver a llamar al hilo de la interfaz de usuario. La forma recomendada de hacerlo es con TaskScheduler.FromSynchronizationContext . Eso solo funciona si tiene un subproceso de interfaz de usuario y no está muy ocupado con otras cosas.

Alois Kraus
fuente
5

La excepción se puede detectar en la función asíncrona.

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();
}
Sanjeevakumar Hiremath
fuente
2
Hola, lo sé, pero realmente necesito esa información en DoFoo para poder mostrar la información en la interfaz de usuario. En este caso, es importante que la interfaz de usuario muestre la excepción, ya que no es una herramienta de usuario final sino una herramienta para depurar un protocolo de comunicación
TimothyP
En ese caso, las devoluciones de llamada tienen mucho sentido (buenos viejos delegados asíncronos)
Sanjeevakumar Hiremath
@Tim: ¿Incluye la información que necesita en la excepción lanzada?
Eric J.
5

También es importante tener en cuenta que perderá el rastro cronológico de la pila de la excepción si tiene un tipo de retorno nulo en un método asíncrono. Recomendaría devolver la tarea de la siguiente manera. Voy a hacer que la depuración sea mucho más fácil.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
rohanjansen
fuente
Esto causará un problema con que no todas las rutas devuelven un valor, ya que si hay una excepción no se devuelve ningún valor, mientras que en el intento sí. Si no tiene ninguna returndeclaración, este código funciona, ya que Taskse devuelve "implícitamente" mediante el uso async / await.
Matias Grioni
2

Este blog explica su problema de forma ordenada. Mejores prácticas asíncronas .

La esencia de esto es que no deberías usar void como retorno para un método asíncrono, a menos que sea un controlador de eventos asíncrono, esta es una mala práctica porque no permite detectar excepciones ;-).

La mejor práctica sería cambiar el tipo de retorno a Tarea. Además, intente codificar asíncrono hasta el final, realice todas las llamadas a métodos asincrónicos y reciba llamadas de métodos asincrónicos. Excepto por un método Main en una consola, que no puede ser asíncrono (antes de C # 7.1).

Te encontrarás con puntos muertos con aplicaciones GUI y ASP.NET si ignoras esta práctica recomendada. El punto muerto se produce porque estas aplicaciones se ejecutan en un contexto que permite solo un subproceso y no lo cederá al subproceso asíncrono. Esto significa que la GUI espera sincrónicamente un retorno, mientras que el método asíncrono espera el contexto: punto muerto.

Este comportamiento no ocurrirá en una aplicación de consola, porque se ejecuta en contexto con un grupo de subprocesos. El método asíncrono volverá en otro hilo que se programará. Es por eso que una aplicación de consola de prueba funcionará, pero las mismas llamadas se estancarán en otras aplicaciones ...

Stephan Ghequiere
fuente
1
"Excepto por un método Main en una consola, que no puede ser asíncrono". Desde C # 7.1, Main ahora puede ser un enlace de
Adam el