asíncrono / espera: ¿cuándo devolver una tarea vs vacío?

503

¿Bajo qué escenarios uno querría usar

public async Task AsyncMethod(int num)

en vez de

public async void AsyncMethod(int num)

El único escenario en el que puedo pensar es si necesita la tarea para poder seguir su progreso.

Además, en el siguiente método, ¿son innecesarias las palabras clave asíncronas y en espera?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}
usuario981225
fuente
20
Tenga en cuenta que los métodos asincrónicos siempre deben tener el sufijo Async . El ejemplo Foo()sería FooAsync().
Fred
30
@ Fred Principalmente, pero no siempre. Esta es solo la convención y las excepciones aceptadas a esta convención son con clases basadas en eventos o contratos de interfaz, vea MSDN . Por ejemplo, no debe cambiar el nombre de los controladores de eventos comunes, como Button1_Click.
Ben
14
Solo una nota que no deberías usar Thread.Sleepcon tus tareas que deberías en su await Task.Delay(num)lugar
Bob Vale
45
@fred No estoy de acuerdo con esto, la OMI que agrega un sufijo asíncrono solo debe usarse cuando proporciona una interfaz con opciones de sincronización y asíncrona. Pitufo nombrar cosas con asíncrono cuando solo hay una intención no tiene sentido. El caso en cuestión Task.Delayno es Task.AsyncDelayque todos los métodos en la tarea sean asíncronos
no amado el
11
Tuve un problema interesante esta mañana con un método de controlador webapi 2, se declaró como en su async voidlugar async Task. El método se bloqueó porque estaba usando un objeto de contexto de Entity Framework declarado como miembro del controlador antes de que el método terminara de ejecutarse. El marco eliminó el controlador antes de que su método terminara de ejecutarse. Cambié el método a Tarea asíncrona y funcionó.
costa

Respuestas:

417

1) Normalmente, desearía devolver a Task. La principal excepción debería ser cuando necesita tener un voidtipo de retorno (para eventos). Si no hay ninguna razón para no permitir que la persona que llama realice awaitsu tarea, ¿por qué no la permite?

2) los asyncmétodos que regresan voidson especiales en otro aspecto: representan operaciones asincrónicas de nivel superior y tienen reglas adicionales que entran en juego cuando su tarea devuelve una excepción. La forma más fácil es mostrar la diferencia con un ejemplo:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fLa excepción es siempre "observada". Una excepción que deja un método asincrónico de nivel superior simplemente se trata como cualquier otra excepción no controlada. gLa excepción nunca se observa. Cuando el recolector de basura viene a limpiar la tarea, ve que la tarea resultó en una excepción, y nadie manejó la excepción. Cuando eso sucede, el TaskScheduler.UnobservedTaskExceptioncontrolador se ejecuta. Nunca debes dejar que esto suceda. Para usar tu ejemplo,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Sí, use asyncy awaitaquí, se aseguran de que su método aún funcione correctamente si se produce una excepción.

Para obtener más información, consulte: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx

suizo
fuente
10
Quise decir en flugar de gen mi comentario. La excepción de fse pasa a SynchronizationContext. gaumentará UnobservedTaskException, pero UTEya no bloquea el proceso si no se maneja. Hay algunas situaciones en las que es aceptable tener "excepciones asincrónicas" como esta que se ignoran.
Stephen Cleary
3
Si tiene WhenAnyvarios Taskcorreos electrónicos que resultan en excepciones. A menudo solo tiene que manejar el primero, y a menudo quiere ignorar a los demás.
Stephen Cleary
1
@StephenCleary Gracias, supongo que es un buen ejemplo, aunque depende de la razón por la que llames WhenAnyen primer lugar si está bien ignorar las otras excepciones: el caso de uso principal que tengo para él todavía termina esperando las tareas restantes cuando finalice alguna. , con o sin una excepción.
10
Estoy un poco confundido sobre por qué recomienda devolver una tarea en lugar de anular. Como dijiste, f () arrojará una excepción, pero g () no. ¿No es mejor estar al tanto de estas excepciones de subprocesos de fondo?
user981225
2
@ user981225 De hecho, pero eso se convierte en responsabilidad de la persona que llama g: cada método que llame a g debe ser asíncrono y esperar también. Esta es una guía, no una regla difícil, puede decidir que en su programa específico, es más fácil tener g return anular.
40

Me he encontrado con este artículo muy útil sobre asyncy voidescrito por Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

La conclusión es que un async+voidsistema puede bloquear el sistema y, por lo general, solo debe usarse en los controladores de eventos secundarios de la interfaz de usuario.

La razón detrás de esto es el contexto de sincronización utilizado por AsyncVoidMethodBuilder, que no es ninguno en este ejemplo. Cuando no hay un contexto de sincronización ambiental, cualquier excepción que no sea manejada por el cuerpo de un método de vacío asíncrono se vuelve a lanzar en el ThreadPool. Si bien aparentemente no hay otro lugar lógico donde se pueda lanzar ese tipo de excepción no controlada, el desafortunado efecto es que el proceso se está terminando, porque las excepciones no controladas en ThreadPool terminan efectivamente el proceso desde .NET 2.0. Puede interceptar todas las excepciones no controladas utilizando el evento AppDomain.UnhandledException, pero no hay forma de recuperar el proceso de este evento.

Al escribir manejadores de eventos de IU, los métodos de vacío asíncrono son de alguna manera indoloros porque las excepciones se tratan de la misma manera que en los métodos no asíncronos; son arrojados al despachador. Existe la posibilidad de recuperarse de tales excepciones, y es más que correcto para la mayoría de los casos. Sin embargo, fuera de los controladores de eventos de UI, los métodos de vacío asíncrono son de alguna manera peligrosos de usar y pueden no ser tan fáciles de encontrar.

Davide Icardi
fuente
¿Es eso correcto? Pensé que las excepciones dentro de los métodos asíncronos se capturan dentro de la Tarea. Y también afaik siempre hay un contexto de sincronización predeterminado
NM
30

Tengo una idea clara de estas declaraciones.

  1. 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 (SynchronizationContext representa una ubicación "donde" podría ejecutarse el código) que estaba activo cuando el método de vacío asíncrono empezado

Las excepciones de un método de vacío asíncrono no se pueden atrapar con Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Estas excepciones se pueden observar usando AppDomain.UnhandledException o un evento general similar para aplicaciones GUI / ASP.NET, pero el uso de esos eventos para el manejo regular de excepciones es una receta para la imposibilidad de mantenimiento (bloquea la aplicación).

  1. Los métodos de vacío asíncrono tienen diferentes semánticas de composición. Los métodos asíncronos que devuelven Tarea o Tarea pueden componerse fácilmente usando await, Task.WhenAny, Task.WhenAll, etc. Los métodos asíncronos que devuelven nulo no proporcionan una manera fácil de notificar al código de llamada que han completado. Es fácil iniciar varios métodos de vacío asíncrono, pero no es fácil determinar cuándo han terminado. Los métodos de vacío asíncrono notificarán su SynchronizationContext cuando comiencen y terminen, pero un SynchronizationContext personalizado es una solución compleja para el código de aplicación normal.

  2. El método Async Void es útil cuando se usa el controlador de eventos síncronos porque generan sus excepciones directamente en el SynchronizationContext, que es similar a cómo se comportan los controladores de eventos síncronos

Para obtener más detalles, consulte este enlace https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Nayas Subramanian
fuente
21

El problema con llamar a async void es que ni siquiera recupera la tarea, no tiene forma de saber cuándo se completó la tarea de la función (consulte https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Estas son las tres formas de llamar a una función asíncrona:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

En todos los casos, la función se transforma en una cadena de tareas. La diferencia es lo que devuelve la función.

En el primer caso, la función devuelve una tarea que finalmente produce la t.

En el segundo caso, la función devuelve una tarea que no tiene producto, pero aún puede esperar para saber cuándo se ha ejecutado hasta su finalización.

El tercer caso es el desagradable. El tercer caso es como el segundo caso, excepto que ni siquiera recuperas la tarea. No tiene forma de saber cuándo se completó la tarea de la función.

El caso de vacío asíncrono es un "dispara y olvida": comienzas la cadena de tareas, pero no te importa cuándo está terminada. Cuando la función regresa, todo lo que sabe es que se ha ejecutado todo hasta la primera espera. Todo después de la primera espera se ejecutará en un punto no especificado en el futuro al que no tendrá acceso.

usuario8128167
fuente
6

Creo que también puede usar async voidpara iniciar operaciones en segundo plano, siempre que tenga cuidado de detectar excepciones. Pensamientos?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}
bboyle1234
fuente
1
El problema con llamar a async void es que ni siquiera recuperas la tarea, no tienes forma de saber cuándo se completó la tarea de la función
usuario8128167
Esto tiene sentido para las operaciones en segundo plano (raras) en las que no le importa el resultado y no desea que una excepción afecte a otras operaciones. Un caso de uso típico sería enviar un evento de registro a un servidor de registro. Desea que esto suceda en segundo plano, y no desea que su controlador de servicio / solicitud falle si el servidor de registro está inactivo. Por supuesto, debe atrapar todas las excepciones, o su proceso será terminado. ¿O hay una mejor manera de lograr esto?
Florian Winter
Las excepciones de un método de vacío asíncrono no se pueden atrapar con Catch. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi
3
@SiZi la captura está DENTRO del método de vacío asíncrono como se muestra en el ejemplo y se captura.
bboyle1234
¿Qué pasa cuando sus requisitos cambian para requerir no hacer trabajo si no puede iniciar sesión? Entonces debe cambiar las firmas en toda su API, en lugar de simplemente cambiar la lógica que actualmente tiene // Ignorar Excepción
Milney
0

Mi respuesta es simple: no puedes esperar método nulo

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Entonces, si el método es asíncrono, es mejor estar atento, ya que puede perder la ventaja asíncrona.

Serg Shevchenko
fuente
-2

De acuerdo con la documentación de Microsoft , NUNCA debe usarasync void

No haga esto: el siguiente ejemplo utiliza async voidlo que completa la solicitud HTTP cuando se alcanza la primera espera:

  • Lo que SIEMPRE es una mala práctica en las aplicaciones ASP.NET Core.

  • Accede a HttpResponse después de completar la solicitud HTTP.

  • Bloquea el proceso.

Robouste
fuente