Partekatu honen bidez:


Escenarios de programación asincrónica

Si el código implementa escenarios enlazados a E/S para admitir solicitudes de datos de red, acceso a bases de datos o lecturas y escrituras del sistema de archivos, la programación asincrónica es el mejor enfoque. También puede escribir código asincrónico para escenarios enlazados a CPU, como cálculos costosos.

C# tiene un modelo de programación asincrónico de nivel de lenguaje que permite escribir fácilmente código asincrónico sin tener que hacer malabares con las devoluciones de llamada o ajustarse a una biblioteca que admita la asincronía. El modelo sigue lo que se conoce como patrón asincrónico basado en tareas (TAP).

Exploración del modelo de programación asincrónica

Los Task objetos y Task<T> representan el núcleo de la programación asincrónica. Estos objetos se usan para modelar operaciones asincrónicas admitiendo las async palabras clave y await . En la mayoría de los casos, el modelo es bastante sencillo para escenarios enlazados a E/S y enlazados a CPU. Dentro de un método async:

  • El código enlazado a E/S inicia una operación representada por un Task objeto o Task<T> dentro del async método .
  • El código limitado por la CPU inicia una operación en un hilo de fondo con el método Task.Run.

En ambos casos, un activo Task representa una operación asincrónica que podría no completarse.

La palabra clave await es donde ocurre la magia. Produce control al autor de la llamada del método que contiene la await expresión y, en última instancia, permite que la interfaz de usuario tenga capacidad de respuesta o que un servicio sea elástico. Aunque hay maneras de abordar código asincrónico distinto del uso de las async expresiones y await , este artículo se centra en las construcciones de nivel de lenguaje.

Nota:

Algunos ejemplos que se presentan en este artículo usan la System.Net.Http.HttpClient clase para descargar datos de un servicio web. En el código de ejemplo, el s_httpClient objeto es un campo estático de clase de tipo Program :

private static readonly HttpClient s_httpClient = new();

Para obtener más información, vea el código de ejemplo completo al final de este artículo.

Revisión de los conceptos subyacentes

Al implementar la programación asincrónica en el código de C#, el compilador transforma el programa en una máquina de estado. Esta construcción realiza un seguimiento de varias operaciones y estados en el código, como ceder la ejecución cuando el código alcanza una await expresión, y reanudar la ejecución cuando se completa una tarea en segundo plano.

En términos de la teoría de la informática, la programación asincrónica es una implementación del modelo de promesa de asincronía.

En el modelo de programación asincrónica, hay varios conceptos clave para comprender:

  • Puede usar código asincrónico para código enlazado a E/S y enlazado a CPU, pero la implementación es diferente.
  • El código asincrónico usa Task<T> objetos y Task como construcciones para modelar el trabajo que se ejecuta en segundo plano.
  • La async palabra clave declara un método como un método asincrónico, que permite usar la await palabra clave en el cuerpo del método.
  • Cuando se aplica la await palabra clave , el código suspende el método de llamada y devuelve el control a su llamador hasta que se completa la tarea.
  • Solo puede usar la await expresión en un método asincrónico.

Ejemplo enlazado a E/S: descarga de datos de un servicio web

En este ejemplo, cuando el usuario selecciona un botón, la aplicación descarga datos de un servicio web. No desea bloquear el subproceso de la interfaz de usuario de la aplicación durante el proceso de descarga. El código siguiente realiza esta tarea:

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

El código expresa la intención (descargar datos de forma asincrónica) sin verse obstaculizado en la interacción con objetos Task.

Ejemplo dependiente de la CPU: Realizar cálculos del juego

En el ejemplo siguiente, un juego móvil inflige daño a varios agentes en la pantalla en respuesta a un evento de presionar un botón. La realización del cálculo del daño puede ser costosa. La ejecución del cálculo en el subproceso de la interfaz de usuario puede provocar problemas de interacción de la interfaz de usuario y de visualización durante el cálculo.

La mejor manera de gestionar la tarea es iniciar un subproceso en segundo plano para completar el trabajo con el método Task.Run. La operación se realiza usando una expresión await. La operación se reanuda cuando se completa la tarea. Este enfoque permite que la interfaz de usuario se ejecute sin problemas mientras el trabajo se completa en segundo plano.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

El código expresa claramente la intención del evento de botón Clicked . No requiere administrar manualmente un subproceso en segundo plano y completa la tarea de forma no bloqueante.

Reconocimiento de escenarios enlazados a CPU y enlazados a E/S

En los ejemplos anteriores se muestra cómo usar el async modificador y la await expresión para el trabajo ligado a E/S y CPU. Un ejemplo para cada escenario muestra cómo el código es diferente en función de dónde está enlazada la operación. Para prepararse para la implementación, debe comprender cómo identificar cuándo una operación está enlazada a E/S o enlazada a cpu. La elección de implementación puede afectar considerablemente al rendimiento del código y, posiblemente, provocar errores de uso de construcciones.

Hay dos preguntas principales que abordar antes de escribir cualquier código:

Pregunta Escenario Implementación
¿Debe esperar el código un resultado o una acción, como datos de una base de datos? Limitado por E/S Use el modificador async y la expresión awaitsin el método Task.Run.

Evite usar la biblioteca paralela de tareas.
¿El código debe ejecutar un cálculo costoso? Limitado por la CPU Use el modificador async y la expresión await, pero genere el trabajo en otro subproceso con el método Task.Run. Este enfoque aborda los problemas con la capacidad de respuesta de la CPU.

Si el trabajo es adecuado para la simultaneidad y el paralelismo, también debe plantearse el uso de la biblioteca TPL.

Mida siempre la ejecución del código. Es posible que descubra que el trabajo enlazado a la CPU no es lo suficientemente costoso en comparación con la sobrecarga de los conmutadores de contexto cuando se realiza multithreading. Todas las opciones tienen inconvenientes. Elija el equilibrio correcto para su situación.

Exploración de otros ejemplos

En los ejemplos de esta sección se muestran varias maneras de escribir código asincrónico en C#. Cubren algunos escenarios con los que podría encontrarse.

Extracción de datos de una red

El código siguiente descarga HTML de una dirección URL determinada y cuenta el número de veces que se produce la cadena ".NET" en el CÓDIGO HTML. El código usa ASP.NET para definir un método de controlador de API web, que realiza la tarea y devuelve el recuento.

Nota:

Si tiene previsto realizar un análisis HTML en el código de producción, no use expresiones regulares. Use una biblioteca de análisis en su lugar.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Puedes escribir código similar para una aplicación universal de Windows y realizar la tarea de recuento después de presionar un botón:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // It's important to do the extra work here before the "await" call,
    // so the user sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This action is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Esperar a que se completen varias tareas

En algunos escenarios, el código debe recuperar varios fragmentos de datos simultáneamente. Las Task API proporcionan métodos que permiten escribir código asincrónico que realiza una espera sin bloqueo en varios trabajos en segundo plano:

En el siguiente ejemplo se muestra cómo obtener datos de User para un conjunto de objetos userId.

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Puede escribir este código de forma más concisa mediante LINQ:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Aunque se escribe menos código mediante LINQ, tenga cuidado al mezclar LINQ con código asincrónico. LINQ usa la ejecución diferida (o perezosa). Las llamadas asincrónicas no se producen inmediatamente como lo hacen en un foreach bucle, a menos que obligue a la secuencia generada a iterar con una llamada a los métodos .ToList() o .ToArray(). En este ejemplo se usa el Enumerable.ToArray método para realizar la consulta de forma diligente y almacenar los resultados en una matriz. Este enfoque obliga a la instrucción id => GetUserAsync(id) a ejecutar e iniciar la tarea.

Revisión de consideraciones para la programación asincrónica

Con la programación asincrónica, hay varios detalles que se deben tener en cuenta que pueden evitar comportamientos inesperados.

Utilizar 'await' dentro del cuerpo del método 'async()'

Al usar el async modificador, debe incluir una o varias await expresiones en el cuerpo del método. Si el compilador no encuentra la expresión await, el método falla al generar resultados. Aunque el compilador genera una advertencia, el código sigue compilando y el compilador ejecuta el método . La máquina de estado generada por el compilador de C# para el método asincrónico no logra nada, por lo que todo el proceso es muy ineficaz.

Adición del sufijo "Async" a nombres de método asincrónicos

La convención de estilo de .NET es agregar el sufijo "Async" a todos los nombres de método asincrónicos. Este enfoque ayuda a diferenciar más fácilmente entre métodos sincrónicos y asincrónicos. En este escenario no se aplican necesariamente determinados métodos a los que no llama explícitamente el código (como controladores de eventos o métodos de controlador web). Dado que el código no llama explícitamente a estos elementos, el uso de nombres explícitos no es tan importante.

Devolución de 'async void' solo desde controladores de eventos

Los controladores de eventos deben declarar void tipos de retorno y no pueden usar ni devolver objetos Task y Task<T> como lo hacen otros métodos. Al escribir controladores de eventos asincrónicos, debe usar el modificador async en un método que devuelva void para los controladores. Otras implementaciones de métodos que devuelven async void no siguen el modelo TAP y pueden presentar desafíos:

  • Las excepciones producidas en un método async void no se pueden detectar fuera de ese método
  • async void los métodos son difíciles de probar
  • Los métodos async void pueden provocar efectos secundarios negativos si el autor de la llamada no espera que sean asincrónicos.

Tenga cuidado con las expresiones lambda asincrónicas en LINQ

Es importante tener cuidado al implementar expresiones lambda asincrónicas en expresiones LINQ. Las expresiones lambda de LINQ usan la ejecución diferida, lo que significa que el código se puede ejecutar en un momento inesperado. La introducción de tareas de bloqueo en este escenario puede dar lugar fácilmente a un interbloqueo, si el código no está escrito correctamente. Además, el anidamiento de código asincrónico también puede dificultar razonar sobre la ejecución del código. Async y LINQ son eficaces, pero estas técnicas deben usarse juntas lo más cuidadosa y claramente posible.

Rendimiento de las tareas de forma no bloqueante

Si tu programa necesita el resultado de una tarea, escribe código que implemente la expresión await de forma no bloqueante. Bloquear el subproceso actual para esperar de forma sincrónica a que un elemento Task se complete puede causar interbloqueos y que se bloqueen los subprocesos de contexto. Este enfoque de programación puede requerir un control de errores más complejo. En la tabla siguiente se proporcionan instrucciones sobre cómo obtener acceso a los resultados de las tareas de forma no desbloqueada:

Escenario de tareas Código actual Reemplazar por "await"
Recuperar el resultado de una tarea en segundo plano Task.Wait o Task.Result await
Continuar cuando se complete cualquier tarea Task.WaitAny await Task.WhenAny
Continuar cuando se completen todas las tareas Task.WaitAll await Task.WhenAll
Continuar después de una cantidad de tiempo Thread.Sleep await Task.Delay

Considere la posibilidad de usar el tipo ValueTask

Cuando un método asincrónico devuelve un objeto Task, es posible que se introduzcan cuellos de botella de rendimiento en ciertas rutas. Dado que Task es un tipo de referencia, se asigna un Task objeto desde el montón. Si un método declarado con el async modificador devuelve un resultado almacenado en caché o se completa sincrónicamente, las asignaciones adicionales pueden acumular costos de tiempo significativos en secciones críticas de rendimiento del código. Este escenario puede resultar costoso cuando las asignaciones se producen en bucles estrechos. Para obtener más información, consulte Tipos de valor devueltos asincrónicos generalizados.

Comprender cuándo establecer ConfigureAwait(false)

A menudo, los desarrolladores preguntan cuándo usar el Task.ConfigureAwait(Boolean) valor booleano. Esta API permite a una Task instancia configurar el contexto de la máquina de estado que implementa cualquier await expresión. Cuando el valor booleano no se establece correctamente, el rendimiento puede degradarse o pueden producirse interbloqueos. Para más información, consulte Preguntas más frecuentes sobre ConfigureAwait.

Escriba código con menos estados

Evite escribir código que dependa del estado de los objetos globales o de la ejecución de determinados métodos. En su lugar, dependa únicamente de los valores devueltos de los métodos. Hay muchas ventajas al escribir código que sea menos dependiente del estado.

  • Más fácil de razonar sobre el código
  • Más fácil de probar el código
  • Más sencillo de mezclar código asincrónico y sincrónico
  • Permite evitar condiciones de carrera en el código
  • Fácil de coordinar código asincrónico que depende de los valores devueltos
  • (Bonus) Funciona bien con la inserción de dependencias en el código

Un objetivo recomendado es lograr una transparencia referencial completa o casi completa en el código. Este enfoque da como resultado un código base predecible, probable y fácil de mantener.

Revisión del ejemplo completo

El código siguiente representa el ejemplo completo, que está disponible en el archivo de ejemplo Program.cs .

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine($"Displaying data: {stringData}");
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.