En pocas palabras: mejores prácticas asíncronas / esperadas en .NET

En previsión del comienzo del curso, "C # Developer" preparó una traducción de material interesante.




Async / Await - Introducción


La construcción del lenguaje Async / Await ha existido desde C # versión 5.0 (2012) y rápidamente se convirtió en uno de los pilares de la programación moderna .NET: cualquier desarrollador de C # que se precie debería usarlo para mejorar el rendimiento de la aplicación, la capacidad de respuesta general y la legibilidad del código.

Async / Await hace que la introducción de código asincrónico sea engañosamente simple y elimina la necesidad de que el programador comprenda los detalles de su procesamiento, pero ¿cuántos de nosotros sabemos realmente cómo funciona y cuáles son las ventajas y desventajas de este método? Hay mucha información útil, pero está fragmentada, así que decidí escribir este artículo.

Bueno, entonces, profundicemos en el tema.

Máquina de estado (IAsyncStateMachine)


Lo primero que debe saber es que, cada vez que tiene un método o función con Async / Await, el compilador convierte su método en una clase generada que implementa la interfaz IAsyncStateMachine. Esta clase es responsable de mantener el estado de su método durante el ciclo de vida de una operación asincrónica: encapsula todas las variables de su método en forma de campos y divide su código en secciones que se ejecutan durante las transiciones de la máquina de estados entre estados, para que el hilo pueda abandonar el método y cuando volverá, el estado no cambiará.

Como ejemplo, aquí hay una definición de clase muy simple con dos métodos asincrónicos:

usar System.Threading.Tasks;

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

Una clase con dos métodos asincrónicos

Si observamos el código generado durante el ensamblaje, veremos algo como esto:



Tenga en cuenta que tenemos 2 nuevas clases internas generadas para nosotros, una para cada método asincrónico. Estas clases contienen una máquina de estado para cada uno de nuestros métodos asincrónicos.

Además, después de estudiar el código descompilado <AsyncAwaitExample> d__0, notaremos que nuestra variable interna «myVariable»ahora es un campo de clase:



también podemos ver otros campos de clase utilizados internamente para mantener el estado IAsyncStateMachine. La máquina de estados pasa por estados utilizando el métodoMoveNext(), de hecho, un gran interruptor. Observe cómo el método continúa en diferentes secciones después de cada una de las llamadas asincrónicas (con la etiqueta de continuación anterior).



Eso significa que la elegancia asíncrona / espera tiene un precio. El uso de async / await en realidad agrega algo de complejidad (de la que puede no ser consciente). En la lógica del lado del servidor, esto puede no ser crítico, pero en particular al programar aplicaciones móviles que tienen en cuenta cada ciclo de memoria de CPU y KB, debe tener esto en cuenta, ya que la cantidad de sobrecarga puede aumentar rápidamente. Más adelante en este artículo, discutiremos las mejores prácticas para usar Async / Await solo cuando sea necesario.

Para una explicación bastante instructiva de la máquina de estado, mira este video en YouTube.

Cuándo usar Async / Await


Generalmente hay dos escenarios en los que Async / Await es la solución correcta.

  • Trabajo relacionado con E / S : su código esperará algo, como datos de una base de datos, leer un archivo, llamar a un servicio web. En este caso, debe usar Async / Await, no la Biblioteca de tareas paralelas.
  • Trabajo relacionado con la CPU : su código realizará cálculos complejos. En este caso, debe usar Async / Await, pero debe comenzar a trabajar en otro hilo usando Task.Run. También puede considerar el uso de la Biblioteca paralela de tareas .



Asíncrono todo el camino


Cuando comience a trabajar con métodos asincrónicos, notará rápidamente que la naturaleza asincrónica del código comienza a extenderse hacia arriba y hacia abajo en su jerarquía de llamadas; esto significa que también debe hacer que su código de llamada sea asincrónico, y así sucesivamente.
Puede tener la tentación de "detener" esto bloqueando el código usando Task.Result o Task.Wait, convirtiendo una pequeña parte de la aplicación y envolviéndola en una API síncrona para que el resto de la aplicación esté aislada de los cambios. Desafortunadamente, esta es una receta para crear puntos muertos difíciles de rastrear.

La mejor solución a este problema es permitir que el código asíncrono crezca en la base de código de forma natural. Si sigue esta decisión, verá la extensión del código asincrónico a su punto de entrada, generalmente un controlador de eventos o una acción de controlador. ¡Ríndete a la asincronía sin dejar rastro!

Más información en este artículo de MSDN.

Si el método se declara asíncrono, ¡asegúrese de que esté esperando!


Como discutimos, cuando el compilador encuentra un método asíncrono, convierte este método en una máquina de estados. Si su código no tiene en espera en su cuerpo, el compilador generará una advertencia, pero la máquina de estado, sin embargo, se creará, agregando una sobrecarga innecesaria para una operación que nunca se completará.

Evitar vacío asíncrono


El vacío asíncrono es algo que realmente debería evitarse. Establezca una regla para usar Tarea asíncrona en lugar de anulación asíncrona.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

El vacío asíncrono y los métodos de tareas asíncronas

Hay varias razones para esto, que incluyen:

  • Las excepciones lanzadas en el método vacío asíncrono no se pueden detectar fuera de este método :
cuando se lanza una excepción desde la Tarea asíncrona o el método de Tarea asíncrona <T >, esta excepción se captura y se coloca en el objeto Tarea. Cuando se utilizan métodos de vacío asíncrono, no hay ningún objeto de tarea, por lo que cualquier excepción lanzada desde el método de vacío asíncrono se invocará directamente en el SynchronizationContext, que estaba activo cuando se ejecutó el método de vacío asíncrono.

Considere el siguiente ejemplo. El bloque de captura nunca será alcanzado.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

Las excepciones lanzadas en el método de vacío asíncrono no se pueden detectar fuera de este método.

Compare con este código, donde en lugar de vacío anímico tenemos una tarea asíncrona. En este caso, la captura será accesible.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

La excepción se detecta y se coloca en el objeto Tarea.

  • Los métodos anulados asíncronos pueden causar efectos secundarios no deseados si la persona que llama no espera que sean asíncronos : si su método asincrónico no devuelve nada, use la Tarea asincrónica (sin una " <T >" para la Tarea) como el tipo de retorno.
  • Los métodos de vacío asíncrono son muy difíciles de probar : debido a las diferencias en el manejo y diseño de errores, es difícil escribir pruebas unitarias que llamen métodos de vacío asíncrono. Prueba asíncrona MSTest sólo funciona para métodos asincrónicos que devuelven una tarea o tareas <T >.

Una excepción a esta práctica son los controladores de eventos asíncronos. Pero incluso en este caso, se recomienda minimizar el código escrito en el propio controlador; espere un método de tarea asíncrono que contenga lógica.

Más información en este artículo de MSDN.

Prefiere la tarea de devolución en lugar de esperar


Como ya se discutió, cada vez que declara un método como asíncrono, el compilador crea una clase de máquina de estado que en realidad envuelve la lógica de su método. Esto agrega cierta sobrecarga que puede acumularse, especialmente para dispositivos móviles, donde tenemos límites de recursos más estrictos.

A veces, un método no tiene que ser asíncrono, pero devuelve la Tarea <T >y permite que el otro lado lo maneje en consecuencia. Si la última oración de su código es un retorno en espera, debería considerar refactorizarlo para que el tipo de retorno del método sea la Tarea <T>(en lugar de async T). Debido a esto, evita generar una máquina de estado, lo que hace que su código sea más flexible. El único caso que realmente queremos esperar es cuando hacemos algo con el resultado de la tarea asíncrona en la continuación del método.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

Prefiera la tarea de devolución en lugar de la espera de devolución

Tenga en cuenta que si no tenemos la espera y, en su lugar, devolvemos la Tarea <T >, la devolución se produce de inmediato, por lo que si el código está dentro de un bloque try / catch, no se detectará la excepción. Del mismo modo, si el código está dentro del bloque de uso, eliminará inmediatamente el objeto. Ver el siguiente consejo.

No envuelva la tarea de retorno dentro de try..catch {} o usando bloques {}


La tarea de retorno puede causar un comportamiento indefinido cuando se usa dentro de un bloque try..catch (nunca se detectará una excepción lanzada por el método asincrónico) o dentro de un bloque de uso, porque la tarea se devolverá de inmediato.

Si necesita ajustar su código asincrónico en un intento ... capturar o usar un bloque, use return waitit en su lugar.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

No envuelva la tarea de devolución dentro de bloques try..catch{}ousing{} .

Más información en este hilo sobre desbordamiento de pila.

Evite usar .Wait()o .Result- use en su lugarGetAwaiter().GetResult()


Si necesita bloquear la espera de que se complete la tarea asincrónica, use GetAwaiter().GetResult(). Waity Resultagregue cualquier excepción AggregateException, lo que complica el manejo de errores. La ventaja GetAwaiter().GetResult()es que en su lugar devuelve la excepción habitual AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

Si necesita bloquear la espera de que se complete la tarea asincrónica, use la GetAwaiter().GetResult().

información Más en este enlace .

Si el método es asíncrono, agregue el sufijo asíncrono a su nombre


Esta es la convención utilizada en .NET para distinguir más fácilmente entre los métodos síncronos y asíncronos (con la excepción de los controladores de eventos o los métodos de controlador web, pero aún así su código no debería invocarlos explícitamente).

Los métodos de biblioteca asincrónica deben usar Task.ConfigureAwait (false) para mejorar el rendimiento



.NET Framework tiene el concepto de un "contexto de sincronización", que es una forma de "volver a donde estaba antes". Cada vez que una Tarea está esperando, captura el contexto de sincronización actual antes de esperar.

Después de completar la tarea .Post(), se llama al método de contexto de sincronización, que reanuda el trabajo desde donde estaba antes. Esto es útil para volver al hilo de la interfaz de usuario o para volver al mismo contexto ASP.NET, etc.
Al escribir el código de la biblioteca, rara vez necesita volver al contexto en el que se encontraba antes. Cuando se utiliza Task.ConfigureAwait (false), el código ya no intenta reanudarse desde donde estaba antes, sino que, si es posible, el código sale en el hilo que completó la tarea, lo que evita el cambio de contexto. Esto mejora ligeramente el rendimiento y puede ayudar a evitar puntos muertos.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

Normalmente, use ConfigureAwait (falso) para los procesos del servidor y el código de la biblioteca.
Esto es especialmente importante cuando el método de la biblioteca se llama una gran cantidad de veces, para una mejor capacidad de respuesta.

Normalmente, use ConfigureAwait (false) para los procesos del servidor en general. No nos importa qué hilo se use para continuar, a diferencia de las aplicaciones en las que necesitamos volver al hilo de la interfaz de usuario.

Ahora ... En ASP.NET Core, Microsoft ha eliminado el SynchronizationContext, por lo que teóricamente no necesita eso. Pero si escribe código de biblioteca que podría reutilizarse en otras aplicaciones (por ejemplo, la aplicación de interfaz de usuario, ASP.NET heredado, formularios de Xamarin), esto sigue siendo la mejor práctica .

Para una buena explicación de este concepto, vea este video .

Informe de progreso de tarea asincrónica


Un caso de uso bastante común para los métodos asincrónicos es trabajar en segundo plano, liberar el hilo de la interfaz de usuario para otras tareas y mantener la capacidad de respuesta. En este escenario, es posible que desee informar el progreso a la interfaz de usuario para que el usuario pueda monitorear el progreso del proceso e interactuar con la operación.

Para resolver este problema común, .NET proporciona la interfaz IProgress <T >, que proporciona el método Report <T >, que es invocado por una tarea asincrónica para informar el progreso al llamante. Esta interfaz se acepta como un parámetro del método asincrónico: la persona que llama debe proporcionar un objeto que implemente esta interfaz.

.NET proporciona Progress <T >, la implementación predeterminada de IProgress <T >, que en realidad se recomienda, ya que maneja toda la lógica de bajo nivel asociada con guardar y restaurar el contexto de sincronización. Progress <T >también proporciona un evento Action <T y una devolución de llamada >, ambas llamadas cuando una tarea informa sobre el progreso.

Juntos, IProgress <T >y Progress <T >proporcionan una manera fácil de transferir información de progreso de una tarea en segundo plano a un subproceso de interfaz de usuario.

Tenga en cuenta que <T>puede ser un valor simple, como un int o un objeto que proporciona información de progreso contextual, como el porcentaje de finalización, una descripción de cadena de la operación actual, ETA, etc.
Considere con qué frecuencia informa el progreso. Dependiendo de la operación que esté realizando, es posible que sus informes de código progresen varias veces por segundo, lo que puede provocar que la interfaz de usuario sea menos receptiva. En tal escenario, se recomienda informar el progreso a intervalos más grandes.

Más información en este artículo en el blog oficial de Microsoft .NET.

Cancelar tareas asincrónicas


Otro caso de uso común para tareas en segundo plano es la capacidad de cancelar la ejecución. .NET proporciona la clase CancellationToken. El método asincrónico recibe el objeto CancellationToken, que luego es compartido por el código de la parte llamante y el método asincrónico, proporcionando así un mecanismo para la señalización de la cancelación.

En el caso más común, la cancelación se produce de la siguiente manera:

  1. La persona que llama crea un objeto CancellationTokenSource.
  2. La persona que llama llama a la API asincrónica cancelada y pasa CancellationToken desde CancellationTokenSource (CancellationTokenSource.Token).
  3. La persona que llama solicita una cancelación utilizando el objeto CancellationTokenSource (CancellationTokenSource.Cancel ()).
  4. La tarea confirma la cancelación y se cancela a sí misma, generalmente utilizando el método CancellationToken.ThrowIfCancellationRequested.

Tenga en cuenta que para que este mecanismo funcione, deberá escribir código para verificar las cancelaciones solicitadas a intervalos regulares (es decir, en cada iteración de su código o en un punto de interrupción natural en la lógica). Idealmente, después de una solicitud de cancelación, la tarea asincrónica debe cancelarse lo más rápido posible.

Debería considerar el uso de deshacer para todos los métodos que pueden tardar mucho tiempo en completarse.

Más información en este artículo en el blog oficial de Microsoft .NET.

Informe de progreso y cancelación - Ejemplo


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

Esperando por un período de tiempo


Si necesita esperar un momento (por ejemplo, intente nuevamente verificar la disponibilidad del recurso), asegúrese de usar Task.Delay - nunca use Thread.Sleep en este escenario.

Esperando a que se completen varias tareas asincrónicas


Use Task.WaitAny para esperar la finalización de cualquier tarea. Use Task.WaitAll para esperar a que se completen todas las tareas.

¿Tengo que apresurarme para cambiar a C # 7 u 8? Regístrese para un seminario web gratuito para discutir este tema.

All Articles