Programación asincrónicaAsynchronous programming

Si tiene cualquier necesidad enlazada a E/S (por ejemplo, solicitar datos de una red o acceder a una base de datos), deberá usar la programación asincrónica.If you have any I/O-bound needs (such as requesting data from a network or accessing a database), you'll want to utilize asynchronous programming. También podría tener código enlazado a la CPU, como realizar un cálculo costoso, que también es un buen escenario para escribir código asincrónico.You could also have CPU-bound code, such as performing an expensive calculation, which is also a good scenario for writing async code.

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.C# has a language-level asynchronous programming model which allows for easily writing asynchronous code without having to juggle callbacks or conform to a library which supports asynchrony. Sigue lo que se conoce como el modelo asincrónico basado en tareas (TAP).It follows what is known as the Task-based Asynchronous Pattern (TAP).

Información general básica del modelo asincrónicoBasic Overview of the Asynchronous Model

El núcleo de la programación asincrónica son los objetos Task y Task<T>, que modelan las operaciones asincrónicas.The core of async programming is the Task and Task<T> objects, which model asynchronous operations. Son compatibles con las palabras clave async y await.They are supported by the async and await keywords. El modelo es bastante sencillo en la mayoría de los casos:The model is fairly simple in most cases:

Para el código enlazado a E/S, se aplica la palabra clave await a una operación que devuelve un objeto Task o Task<T> dentro de un método async.For I/O-bound code, you await an operation which returns a Task or Task<T> inside of an async method.

Para el código enlazado a la CPU, se aplica la palabra clave await a una operación que se inicia en un subproceso en segundo plano con el método Task.Run.For CPU-bound code, you await an operation which is started on a background thread with the Task.Run method.

La palabra clave await es donde ocurre la magia.The await keyword is where the magic happens. Genera control para el autor de la llamada del método que ha realizado await, y permite en última instancia una interfaz de usuario con capacidad de respuesta o un servicio flexible.It yields control to the caller of the method that performed await, and it ultimately allows a UI to be responsive or a service to be elastic.

Hay otras formas de abordar el código asincrónico aparte de async y await, que se describen en el artículo de TAP indicado anteriormente, pero en este documento nos centraremos en las construcciones de nivel de lenguaje de aquí en adelante.There are other ways to approach async code than async and await outlined in the TAP article linked above, but this document will focus on the language-level constructs from this point forward.

Ejemplo enlazado a E/S: descarga de datos de un servicio webI/O-Bound Example: Downloading data from a web service

Puede que necesite descargar algunos datos de un servicio web cuando se presione un botón, pero no quiera bloquear el subproceso de interfaz de usuario.You may need to download some data from a web service when a button is pressed, but don’t want to block the UI thread. Puede conseguirlo fácilmente de la siguiente forma:It can be accomplished simply like this:

private readonly HttpClient _httpClient = new HttpClient();

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 _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Y listo.And that’s it! El código expresa la intención (descargar algunos datos de forma asincrónica) sin verse obstaculizado en la interacción con objetos Task.The code expresses the intent (downloading some data asynchronously) without getting bogged down in interacting with Task objects.

Ejemplo enlazado a la CPU: realizar un cálculo para un juegoCPU-bound Example: Performing a Calculation for a Game

Supongamos que está escribiendo un juego para móviles en el que se pueden infligir daños a muchos enemigos en la pantalla pulsando un botón.Say you're writing a mobile game where pressing a button can inflict damage on many enemies on the screen. Realizar el cálculo del daño puede resultar costoso y hacerlo en el subproceso de interfaz de usuario haría que pareciera que el juego se pone en pausa mientras se lleva a cabo el cálculo.Performing the damage calculation can be expensive, and doing it on the UI thread would make the game appear to pause as the calculation is performed!

La mejor manera de abordar esta situación consiste en iniciar un subproceso en segundo plano que realice la tarea mediante Task.Run y aplique la palabra clave await para esperar su resultado.The best way to handle this is to start a background thread which does the work using Task.Run, and await its result. Esto permitirá que la interfaz de usuario funcione de manera fluida mientras se lleva a cabo la tarea.This will allow the UI to feel smooth as the work is being done.

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

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

Y listo.And that's it! Este código expresa claramente la intención del evento de clic del botón, no requiere la administración manual de un subproceso en segundo plano y lo hace en un modo sin bloqueo.This code cleanly expresses the intent of the button's click event, it doesn't require managing a background thread manually, and it does so in a non-blocking way.

Qué sucede en segundo planoWhat happens under the covers

En las operaciones asincrónicas existen numerosos aspectos dinámicos.There's a lot of moving pieces where asynchronous operations are concerned. Si siente curiosidad sobre lo que ocurre en el segundo plano de Task y Task<T>, eche un vistazo al artículo Async en profundidad para obtener más información.If you're curious about what's happening underneath the covers of Task and Task<T>, checkout the Async in-depth article for more information.

En lo que respecta a C#, el compilador transforma el código en una máquina de estados que realiza el seguimiento de acciones como la retención de la ejecución cuando se alcanza await y la reanudación de la ejecución cuando se ha finalizado un trabajo en segundo plano.On the C# side of things, the compiler transforms your code into a state machine which keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.

Para los más interesados en la teoría, se trata de una implementación del modelo de promesas de asincronía.For the theoretically-inclined, this is an implementation of the Promise Model of asynchrony.

Piezas clave que debe comprenderKey Pieces to Understand

  • El código asincrónico puede usarse para código tanto enlazado a E/S como enlazado a la CPU, pero de forma distinta en cada escenario.Async code can be used for both I/O-bound and CPU-bound code, but differently for each scenario.
  • El código asincrónico usa Task<T> y Task, que son construcciones que se usan para modelar el trabajo que se realiza en segundo plano.Async code uses Task<T> and Task, which are constructs used to model work being done in the background.
  • La palabra clave async convierte un método en un método asincrónico, lo que permite usar la palabra clave await en su cuerpo.The async keyword turns a method into an async method, which allows you to use the await keyword in its body.
  • Cuando se aplica la palabra clave await, se suspende el método de llamada y se cede el control al autor de la llamada hasta que se completa la tarea esperada.When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete.
  • await solo puede usarse dentro de un método asincrónico.await can only be used inside an async method.

Reconocer el trabajo enlazado a la CPU y el enlazado a E/SRecognize CPU-Bound and I/O-Bound Work

En los dos primeros ejemplos de esta guía se ha explicado cómo se puede usar async y await para trabajos enlazados a E/S y a la CPU.The first two examples of this guide showed how you can use async and await for I/O-bound and CPU-bound work. Resulta fundamental que pueda identificar si el trabajo que debe realizar está enlazado a E/S o a la CPU, ya que esto puede afectar en gran medida al rendimiento del código y podría dar lugar al uso inadecuado de ciertas construcciones.It's key that you can identify when a job you need to do is I/O-bound or CPU-bound, because it can greatly affect the performance of your code and could potentially lead to misusing certain constructs.

A continuación, se indican dos preguntas que debe hacerse antes de escribir el código:Here are two questions you should ask before you write any code:

  1. ¿Estará su código "esperando" algo, como datos de una base de datos?Will your code be "waiting" for something, such as data from a database?

    Si la respuesta es "sí", su trabajo está enlazado a E/S.If your answer is "yes", then your work is I/O-bound.

  2. ¿Realizará el código un cálculo muy costoso?Will your code be performing a very expensive computation?

    Si la respuesta es "sí", su trabajo está enlazado a la CPU.If you answered "yes", then your work is CPU-bound.

Si el trabajo que tiene está enlazado a E/S, use async y await sin Task.Run.If the work you have is I/O-bound, use async and await without Task.Run. No debe usar la Biblioteca TPL.You should not use the Task Parallel Library. Esto se explica en el artículo Async en profundidad.The reason for this is outlined in the Async in Depth article.

Si el trabajo que tiene está enlazado a la CPU y le interesa la capacidad de respuesta, use async y await, pero genere el trabajo en otro subproceso con Task.Run.If the work you have is CPU-bound and you care about responsiveness, use async and await but spawn the work off on another thread with Task.Run. Si el trabajo es adecuado para la simultaneidad y el paralelismo, también debe plantearse el uso de la biblioteca TPL.If the work is appropriate for concurrency and parallelism, you should also consider using the Task Parallel Library.

Además, siempre debe medir la ejecución del código.Additionally, you should always measure the execution of your code. Por ejemplo, puede verse en una situación en la que el trabajo enlazado a la CPU no sea suficientemente costoso en comparación con la sobrecarga de cambios de contexto cuando realice multithreading.For example, you may find yourself in a situation where your CPU-bound work is not costly enough compared with the overhead of context switches when multithreading. Cada opción tiene su compensación y debe elegir el equilibrio correcto para su situación.Every choice has its tradeoff, and you should pick the correct tradeoff for your situation.

Más ejemplosMore Examples

En los ejemplos siguientes se muestran distintas maneras en las que puede escribir código asincrónico en C#.The following examples demonstrate various ways you can write async code in C#. Abarcan algunos escenarios diferentes con los que puede encontrarse.They cover a few different scenarios you may come across.

Extraer datos de una redExtracting Data from a Network

Este fragmento de código descarga el HTML desde la página principal en www.dotnetfoundation.org y cuenta el número de veces que aparece la cadena ".NET" en el código HTML.This snippet downloads the HTML from the homepage at www.dotnetfoundation.org and counts the number of times the string ".NET" occurs in the HTML. Usa ASP.NET MVC para definir un método de controlador web que realiza esta tarea y devuelve el número.It uses ASP.NET MVC to define a web controller method which performs this task, returning the number.

Nota

Si tiene previsto realizar un análisis HTML en el código de producción, no use expresiones regulares.If you plan on doing HTML parsing in production code, don't use regular expressions. Use una biblioteca de análisis en su lugar.Use a parsing library instead.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet]
[Route("DotNetCount")]
public async Task<int> GetDotNetCountAsync()
{
    // Suspends GetDotNetCountAsync() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Este es el mismo escenario escrito para una aplicación Windows Universal, que realiza la misma tarea cuando se presiona un botón:Here's the same scenario written for a Universal Windows App, which performs the same task when a Button is pressed:

private readonly HttpClient _httpClient = new HttpClient();

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

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

    // The await operator suspends SeeTheDotNets_Click, returning control to its caller.
    // This 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 tareasWaiting for Multiple Tasks to Complete

Es posible que se vea en una situación en la que necesite recuperar varios fragmentos de datos al mismo tiempo.You may find yourself in a situation where you need to retrieve multiple pieces of data concurrently. La API Task contiene dos métodos, Task.WhenAll y Task.WhenAny, que permiten escribir código asincrónico que realiza una espera sin bloqueo en varios trabajos en segundo plano.The Task API contains two methods, Task.WhenAll and Task.WhenAny which allow you to write asynchronous code which performs a non-blocking wait on multiple background jobs.

En este ejemplo se muestra cómo podría captar datos User de un conjunto de elementos userId.This example shows how you might grab User data for a set of userIds.

public 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.
}

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

Aquí tiene otra manera de escribir lo mismo de una forma más sucinta, con LINQ:Here's another way to write this a bit more succinctly, using LINQ:

public 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.
}

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

Aunque es menos código, tenga cuidado al combinar LINQ con código asincrónico.Although it's less code, take care when mixing LINQ with asynchronous code. Dado que LINQ usa la ejecución diferida, las llamadas asincrónicas no se realizarán inmediatamente, como lo hacen en un bucle foreach(), a menos que fuerce la secuencia generada a procesar una iteración con una llamada a .ToList() o .ToArray().Because LINQ uses deferred (lazy) execution, async calls won't happen immediately as they do in a foreach() loop unless you force the generated sequence to iterate with a call to .ToList() or .ToArray().

Consejos e información importanteImportant Info and Advice

Aunque la programación asincrónica es relativamente sencilla, hay algunos detalles que debe tener en cuenta para evitar un comportamiento inesperado.Although async programming is relatively straightforward, there are some details to keep in mind which can prevent unexpected behavior.

  • Los métodos async deben tener una palabra clave await en su cuerpo o nunca proporcionarán resultados.async methods need to have an await keyword in their body or they will never yield!

Es importante que tenga esto en cuenta.This is important to keep in mind. Si no se usa await en el cuerpo de un método async, el compilador de C# generará una advertencia, pero el código se compilará y se ejecutará como si se tratara de un método normal.If await is not used in the body of an async method, the C# compiler will generate a warning, but the code will compile and run as if it were a normal method. Tenga en cuenta también que esto sería muy ineficaz, ya que la máquina de estados generada por el compilador de C# para el método asincrónico no realizaría nada.Note that this would also be incredibly inefficient, as the state machine generated by the C# compiler for the async method would not be accomplishing anything.

  • Debe agregar "Async" como el sufijo de todos los métodos asincrónicos que escriba.You should add "Async" as the suffix of every async method name you write.

Se trata de la convención que se usa en .NET para distinguir más fácilmente los métodos sincrónicos de los asincrónicos.This is the convention used in .NET to more-easily differentiate synchronous and asynchronous methods. Tenga en cuenta que no se aplican necesariamente ciertos métodos a los que el código no llame explícitamente (como controladores de eventos o métodos de controlador web).Note that certain methods which aren’t explicitly called by your code (such as event handlers or web controller methods) don’t necessarily apply. Puesto que el código no los llama explícitamente, resulta importante explicitar sus nombres.Because these are not explicitly called by your code, being explicit about their naming isn’t as important.

  • async void solo se debe usar para controladores de eventos.async void should only be used for event handlers.

async void es la única manera de permitir a los controladores de eventos asincrónicos trabajar, ya que los eventos no tienen tipos de valor devuelto (por lo tanto, no pueden hacer uso de Task y Task<T>).async void is the only way to allow asynchronous event handlers to work because events do not have return types (thus cannot make use of Task and Task<T>). Cualquier otro uso de async void no sigue el modelo de TAP y puede resultar difícil de usar, como:Any other use of async void does not follow the TAP model and can be challenging to use, such as:

  • Las excepciones producidas en un método async void no se pueden detectar fuera de ese método.Exceptions thrown in an async void method can’t be caught outside of that method.

  • Los métodos async void resultan muy difíciles de probar.async void methods are very difficult to test.

  • Los métodos async void pueden provocar efectos secundarios negativos si el autor de la llamada no espera que sean asincrónicos.async void methods can cause bad side effects if the caller isn’t expecting them to be async.

  • Tenga cuidado al usar lambdas asincrónicas en las expresiones de LINQ.Tread carefully when using async lambdas in LINQ expressions

Las expresiones lambda de LINQ usan la ejecución aplazada, lo que implica que el código podría acabar ejecutándose en un momento en que no se lo espere.Lambda expressions in LINQ use deferred execution, meaning code could end up executing at a time when you’re not expecting it to. La introducción de las tareas de bloqueo puede dar lugar a un interbloqueo si no se han escrito correctamente.The introduction of blocking tasks into this can easily result in a deadlock if not written correctly. Además, el anidamiento de código asincrónico de esta manera también puede hacer que resulte más difícil razonar sobre la ejecución del código.Additionally, the nesting of asynchronous code like this can also make it more difficult to reason about the execution of the code. Async y LINQ son eficaces, pero deben usarse conjuntamente con el mayor cuidado y claridad posible.Async and LINQ are powerful, but should be used together as carefully and clearly as possible.

  • Escriba código que espere las tareas sin bloqueo.Write code that awaits Tasks in a non-blocking manner

Bloquear el subproceso actual como un medio para esperar que se complete una tarea puede dar lugar a interbloqueos y subprocesos de contexto bloqueados, y puede requerir un control de errores mucho más complejo.Blocking the current thread as a means to wait for a Task to complete can result in deadlocks and blocked context threads, and can require significantly more complex error-handling. En la tabla siguiente se ofrece orientación sobre cómo abordar la espera de las tareas de una manera que no produzca un bloqueo:The following table provides guidance on how to deal with waiting for Tasks in a non-blocking way:

Use esto...Use this... En vez de esto...Instead of this... Cuando quiera hacer estoWhen wishing to do this
await Task.Wait o Task.ResultTask.Wait or Task.Result Recuperar el resultado de una tarea en segundo planoRetrieving the result of a background task
await Task.WhenAny Task.WaitAny Esperar que finalice cualquier tareaWaiting for any task to complete
await Task.WhenAll Task.WaitAll Esperar que finalicen todas las tareasWaiting for all tasks to complete
await Task.Delay Thread.Sleep Esperar un período de tiempoWaiting for a period of time
  • Escriba código con menos estados.Write less stateful code

No dependa del estado de los objetos globales o la ejecución de ciertos métodos.Don’t depend on the state of global objects or the execution of certain methods. En su lugar, dependa únicamente de los valores devueltos de los métodos.Instead, depend only on the return values of methods. ¿Por qué?Why?

  • Le resultará más fácil razonar sobre el código.Code will be easier to reason about.
  • Le resultará más fácil probar el código.Code will be easier to test.
  • Resulta mucho más sencillo mezclar código asincrónico y sincrónico.Mixing async and synchronous code is far simpler.
  • Normalmente se pueden evitar por completo las condiciones de carrera.Race conditions can typically be avoided altogether.
  • Depender de los valores devueltos facilita la coordinación de código asincrónico.Depending on return values makes coordinating async code simple.
  • (Extra) Funciona muy bien con la inserción de dependencias.(Bonus) it works really well with dependency injection.

Un objetivo recomendado es lograr una transparencia referencial completa o casi completa en el código.A recommended goal is to achieve complete or near-complete Referential Transparency in your code. Esto se traducirá en un código base extremadamente predecible, que se puede probar y es fácil de mantener.Doing so will result in an extremely predictable, testable, and maintainable codebase.

Otros recursosOther Resources