Cómo llamar de forma segura a un método asíncrono en C # sin esperar

322

Tengo un asyncmétodo que no devuelve datos:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

Llamo a esto desde otro método que devuelve algunos datos:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Llamar MyAsyncMethod()sin esperar provoca una advertencia " Debido a que esta llamada no se espera, el método actual continúa ejecutándose antes de que se complete la llamada " en Visual Studio. En la página de esa advertencia dice:

Debería considerar suprimir la advertencia solo si está seguro de que no desea esperar a que se complete la llamada asincrónica y que el método llamado no generará ninguna excepción .

Estoy seguro de que no quiero esperar a que se complete la llamada; No necesito ni tengo tiempo para hacerlo. Pero la llamada podría generar excepciones.

Me he topado con este problema varias veces y estoy seguro de que es un problema común que debe tener una solución común.

¿Cómo llamo con seguridad a un método asíncrono sin esperar el resultado?

Actualizar:

Para las personas que sugieren que solo espero el resultado, este es un código que responde a una solicitud web de nuestro servicio web (API web ASP.NET). La espera en un contexto de IU mantiene el subproceso de IU libre, pero en una llamada de solicitud web esperará a que finalice la tarea antes de responder a la solicitud, aumentando así los tiempos de respuesta sin motivo.

George Powell
fuente
¿Por qué no simplemente crear un método de finalización e ignorarlo allí? Porque si se ejecuta en subproceso de fondo. Entonces no detendrá su programa de terminar de todos modos.
Yahya
1
Si no desea esperar el resultado, la única opción es ignorar / suprimir la advertencia. Si no desea esperar a que el resultado / excepción a continuaciónMyAsyncMethod().Wait()
Peter Ritchie
1
Sobre tu edición: eso no tiene sentido para mí. Digamos que la respuesta se envía al cliente 1 segundo después de la solicitud, y 2 segundos después, su método asincrónico arroja una excepción. ¿Qué harías con esa excepción? No puede enviarlo al cliente si su respuesta ya se envió. ¿Qué más harías con él?
2
@Romoku lo suficientemente justo. Asumiendo que alguien mire el registro, de todos modos. :)
2
Una variación en el escenario API ASP.NET Web es un auto-organizada API Web en un proceso de larga duración (como, por ejemplo, un servicio de Windows), cuando una solicitud crea una larga tarea en segundo plano para hacer algo caro, pero todavía quiere obtenga una respuesta rápidamente con un HTTP 202 (aceptado).
David Rubin

Respuestas:

187

Si desea obtener la excepción "asincrónicamente", puede hacer lo siguiente:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Esto le permitirá lidiar con una excepción en un hilo que no sea el hilo "principal". Esto significa que no tiene que "esperar" la llamada MyAsyncMethod()desde el hilo que llama MyAsyncMethod; pero aún le permite hacer algo con una excepción, pero solo si ocurre una excepción.

Actualizar:

técnicamente, podrías hacer algo similar con await:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... que sería útil si necesita utilizar específicamente try/ catch(o using), pero creo que el ContinueWithser un poco más explícito porque hay que saber qué ConfigureAwait(false)medios.

Peter Ritchie
fuente
77
TaskConvertí esto en un método de extensión en : public static class AsyncUtility {public static void PerformAsyncTaskWithoutAwait (this Task task, Action <Task> exceptionHandler) {var dummy = task.ContinueWith (t => exceptionHandler (t), TaskContinuationOptions.OnlyOnFaulted); }} Uso: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat ("Se produjo un error al llamar a MyAsyncMethod: \ n {0}", t.Exception));
Mark Avenius
2
Votante Comentarios? Si hay algo mal en la respuesta, me encantaría saberlo y / o solucionarlo.
Peter Ritchie
2
Hola, no voté en contra, pero ... ¿Puedes explicar tu actualización? Se suponía que la llamada no debía ser esperada, por lo que si lo hace así, entonces espera la llamada, simplemente no continúa en el contexto capturado ...
Bartosz
1
"esperar" no es la descripción correcta en este contexto. La línea después de la ConfiguratAwait(false)no se ejecuta hasta que se completa la tarea, pero el hilo actual no "espera" (es decir, bloquea) para eso, la siguiente línea se llama de forma asíncrona a la invocación de espera. Sin ConfigureAwait(false)eso, la siguiente línea se ejecutaría en el contexto de solicitud web original. Con el ConfigurateAwait(false)se ejecuta en el mismo contexto que el método asincrónico (tarea), liberando el contexto / hilo original para continuar ...
Peter Ritchie
15
La versión ContinueWith no es la misma que la versión try {await} catch {}. En la primera versión, todo lo que sigue a ContinueWith () se ejecutará inmediatamente. La tarea inicial es despedida y olvidada. En la segunda versión, todo después de catch {} se ejecutará solo después de que se complete la tarea inicial. La segunda versión es equivalente a "wait MyAsyncMethod (). ContinueWith (t => Console.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis
67

Primero debe considerar hacer GetStringDataun asyncmétodo y hacer que le awaitdevuelva la tarea MyAsyncMethod.

Si está absolutamente seguro de que no necesita manejar excepciones MyAsyncMethod o saber cuándo se completa, puede hacer esto:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

Por cierto, este no es un "problema común". Es muy raro querer ejecutar algún código y no importa si se completa y no importa si se completa con éxito.

Actualizar:

Dado que está en ASP.NET y desea regresar temprano, puede encontrar útil mi publicación de blog sobre el tema . Sin embargo, ASP.NET no fue diseñado para esto, y no hay garantía de que su código se ejecutará después de que se devuelva la respuesta. ASP.NET hará todo lo posible para que se ejecute, pero no puede garantizarlo.

Entonces, esta es una buena solución para algo simple como lanzar un evento en un registro donde realmente no importa si pierde algunos aquí y allá. No es una buena solución para ningún tipo de operaciones comerciales críticas. En esas situaciones, debe adoptar una arquitectura más compleja, con una forma persistente de guardar las operaciones (por ejemplo, Azure Queues, MSMQ) y un proceso en segundo plano separado (por ejemplo, Azure Worker Role, Win32 Service) para procesarlas.

Stephen Cleary
fuente
2
Creo que puedes haber entendido mal. Yo hago cuidado si se lanza excepciones y falla, pero no quiero tener que esperar el método antes de regresar mis datos. También vea mi edición sobre el contexto en el que estoy trabajando si eso hace alguna diferencia.
George Powell
55
@GeorgePowell: Es muy peligroso tener código ejecutándose en un contexto ASP.NET sin una solicitud activa. Tengo una publicación de blog que puede ayudarlo , pero sin saber más sobre su problema, no puedo decir si recomendaría ese enfoque o no.
Stephen Cleary
@StephenCleary Tengo una necesidad similar. En mi ejemplo, necesito / necesito un motor de procesamiento por lotes para que se ejecute en la nube, haré un "ping" en el punto final para iniciar el procesamiento por lotes, pero quiero regresar de inmediato. Dado que hacer ping hace que comience, puede manejar todo desde allí. Si hay excepciones que se tiran, entonces se acababan de registrar en mis "BatchProcessLog / error" mesas ...
Ganders
8
En C # 7, puede reemplazar var _ = MyAsyncMethod();con _ = MyAsyncMethod();. Esto todavía evita la advertencia CS4014, pero hace que sea un poco más explícito que no está utilizando la variable.
Brian
48

La respuesta de Peter Ritchie fue lo que quería, y el artículo de Stephen Cleary sobre el regreso temprano en ASP.NET fue muy útil.

Sin embargo, como un problema más general (no específico de un contexto ASP.NET), la siguiente aplicación de consola muestra el uso y el comportamiento de la respuesta de Peter usando Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()regresa temprano sin esperar MyAsyncMethod()y las excepciones incluidas MyAsyncMethod()se tratan en OnMyAsyncMethodFailed(Task task)y no en try/ catchalrededorGetStringData()

George Powell
fuente
99
Elimine Console.ReadLine();y agregue un poco de suspensión / retraso MyAsyncMethody nunca verá la excepción.
tymtam
17

Termino con esta solución:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}
Filimindji
fuente
8

Esto se llama disparar y olvidar, y hay una extensión para eso.

Consume una tarea y no hace nada con ella. Útil para disparar y olvidar llamadas a métodos asincrónicos dentro de métodos asincrónicos.

Instalar paquete nuget .

Utilizar:

MyAsyncMethod().Forget();
Wast
fuente
12
No hace nada, es solo un truco para eliminar la advertencia. Ver github.com/microsoft/vs-threading/blob/master/src/…
Paolo Fulgoni
2

Supongo que surge la pregunta, ¿por qué necesitarías hacer esto? La razón asyncen C # 5.0 es para que pueda esperar un resultado. Este método no es realmente asíncrono, sino que simplemente se llama a la vez para no interferir demasiado con el hilo actual.

Quizás sea mejor comenzar un hilo y dejar que termine solo.

rhughes
fuente
2
asynces un poco más que simplemente "esperar" un resultado. "esperar" implica que las líneas que siguen a "esperar" se ejecutan de forma asíncrona en el mismo hilo que invocó "esperar". Esto se puede hacer sin "esperar", por supuesto, pero terminas teniendo un montón de delegados y pierdes el aspecto secuencial del código (así como la capacidad de usar usingy try/catch...
Peter Ritchie
1
@PeterRitchie Puede tener un método que sea funcionalmente asíncrono, no use la awaitpalabra clave y no use la asyncpalabra clave, pero no tiene sentido usar la asyncpalabra clave sin usarla también awaiten la definición de ese método.
Servy
1
@PeterRitchie De la declaración: " asynces un poco más que simplemente" esperar "un resultado". La asyncpalabra clave (usted dio a entender que es la palabra clave al encerrarla en backticks) no significa nada más que esperar el resultado. Es la asincronía, como los conceptos generales de CS, lo que significa más que solo esperar un resultado.
Servy
1
@Servy asynccrea una máquina de estado que gestiona cualquier espera dentro del método asíncrono. Si no hay awaits dentro del método, todavía crea esa máquina de estados, pero el método no es asíncrono. Y si el asyncmétodo regresa void, no hay nada que esperar. Entonces, es más que solo esperar un resultado.
Peter Ritchie
1
@PeterRitchie Mientras el método devuelva una tarea, puede esperarla. No hay necesidad de la máquina de estado o la asyncpalabra clave. Todo lo que necesita hacer (y todo lo que realmente sucede al final con una máquina de estado en ese caso especial) es que el método se ejecuta sincrónicamente y luego se envuelve en una tarea completada. Supongo que técnicamente no solo eliminas la máquina de estado; quita la máquina de estado y luego llama Task.FromResult. Supuse que usted (y también los escritores del compilador) podrían agregar el apéndice por su cuenta.
Servicio
0

En tecnologías con bucles de mensajes (no estoy seguro si ASP es uno de ellos), puede bloquear el bucle y procesar los mensajes hasta que finalice la tarea, y usar ContinueWith para desbloquear el código:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Este enfoque es similar al bloqueo en ShowDialog y sigue manteniendo la interfaz de usuario receptiva.

haimb
fuente
0

Llego tarde a la fiesta aquí, pero hay una biblioteca increíble que he estado usando y que no he visto referenciada en las otras respuestas

https://github.com/brminnick/AsyncAwaitBestPractices

Si necesita "disparar y olvidar", llame al método de extensión en la tarea.

Pasar la acción onException a la llamada asegura que obtenga lo mejor de ambos mundos, sin necesidad de esperar la ejecución y ralentizar a sus usuarios, al tiempo que conserva la capacidad de manejar la excepción de manera elegante.

En su ejemplo, lo usaría así:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

También proporciona los AsyncCommands esperados que implementan ICommand de fábrica, lo que es excelente para mi solución MVVM Xamarin

Adam Diament
fuente
-1

La solución es iniciar el HttpClient en otra tarea de ejecución sin contexto de sincronización:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);
Ernesto Garcia
fuente
-1

Si realmente quieres hacer esto. Solo para abordar "Llamar a un método asincrónico en C # sin esperar", puede ejecutar el método asincrónico dentro de a Task.Run. Este enfoque esperará hasta el MyAsyncMethodfinal.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitdesenvuelve asincrónicamente la Resulttarea, mientras que solo usando Result se bloquearía hasta que la tarea se hubiera completado.

CharithJ
fuente
-1

Por lo general, el método asíncrono devuelve la clase de tarea. Si usa un Wait()método o Resultpropiedad y el código arroja una excepción (el tipo de excepción se envuelve) AggregateException, entonces debe consultar Exception.InnerExceptionpara localizar la excepción correcta.

Pero también es posible usarlo .GetAwaiter().GetResult(): también esperará la tarea asincrónica, pero no envolverá la excepción.

Así que aquí hay un breve ejemplo:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Es posible que también desee poder devolver algunos parámetros de la función asincrónica, lo que se puede lograr al proporcionar una Action<return type>función asincrónica adicional , por ejemplo, así:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

Tenga en cuenta que los métodos asíncronos generalmente tienen ASyncnombres de sufijos, solo para evitar la colisión entre funciones de sincronización con el mismo nombre. (Por ejemplo FileStream.ReadAsync): he actualizado los nombres de las funciones para seguir esta recomendación.

TarmoPikaro
fuente