martes, 22 de octubre de 2019
Hasta ahora, la generación o enumeración de secuencias era un proceso puramente síncrono. Por ejemplo, si queríamos recorrer un
Por ejemplo, en el siguiente código no teníamos una forma razonable de implementarlo si la obtención de cada uno de los valores retornados desde el método generador tuviera que ser asíncrona:
Pues bien, aparte de características mainstream como la implementación por defecto en interfaces, los tipos referencia anulables, índices y rangos o muchas otras, en la última versión del framework y C# 8 se ha introducido el soporte para la generación y consumo de secuencias asíncronas.
En la práctica, esto implica que a partir de ahora se puede iterar sobre una enumeración en la que cada elemento puede ser obtenido de forma asíncrona. Para ello, se han introducido dos cambios en .NET y C#:
Y dado que esta nueva característica abre la puerta a funcionalidades muy interesantes de intercambio de datos en streaming, ya podemos verla reflejada en frameworks como ASP.NET Core o SignalR.
Publicado en Variable not found.
IEnumerable
con un bucle foreach
, cada uno de los elementos debía existir previamente en la colección o bien ser generado de forma síncrona.Por ejemplo, en el siguiente código no teníamos una forma razonable de implementarlo si la obtención de cada uno de los valores retornados desde el método generador tuviera que ser asíncrona:
foreach (var i in GetNumbers())
{
Console.WriteLine(i);
}
IEnumerable<int> GetNumbers()
{
for (var i = 0; i < 1000_000_000; i++)
{
var a = i * 2; // <-- Esto es una operación síncrona,
yield return a; // ¿cómo haríamos si en lugar de esta operación síncrona
// necesitásemos hacer una llamada asíncrona para obtenerlo?
}
}
Aunque convertir el método GetNumbers()
en asíncrono pudiera parecer una alternativa razonable, en realidad no lo es; de hecho, los resultados no llegarían al cliente hasta que hubiéramos generado todos los valores, por lo que sería peor que la primera opción en términos de rendimiento y ocupación de memoria:foreach (var i in await GetNumbersAsync())
{
Console.WriteLine(i);
}
async Task<IEnumerable<int>> GetNumbersAsync()
{
var list = new List<int>();
for (var i = 0; i < 1000_000_000; i++)
{
var a = await Task.FromResult(i * 2); // <-- Aquí generamos los valores usando asincronía,
list.Add(a); // pero el consumidor seguirá esperando hasta
// que los hayamos generado todos.
}
return list; // <-- Aquí retornamos la colección completa
}
En este último caso la llamada a GetNumbersAsync()
se ejecutaría de forma asíncrona, es decir, daríamos la oportunidad al hilo de ejecución actual de dedicarse a otros menesteres mientras la llamada es realizada, desde el punto de vista de su consumidor es a todos los efectos como si se tratara de un método síncrono.Pues bien, aparte de características mainstream como la implementación por defecto en interfaces, los tipos referencia anulables, índices y rangos o muchas otras, en la última versión del framework y C# 8 se ha introducido el soporte para la generación y consumo de secuencias asíncronas.
En la práctica, esto implica que a partir de ahora se puede iterar sobre una enumeración en la que cada elemento puede ser obtenido de forma asíncrona. Para ello, se han introducido dos cambios en .NET y C#:
-
Primero, en lugar de
IEnumerable<T>
, los métodos o funciones generadoras deberán utilizarIAsyncEnumerable<T>
. Básicamente es lo mismo que antes, pero indicamos que cada elemento de la secuencia se generará de forma asíncrona.
-
Segundo, para iterar sobre los elementos utilizaremos la construcción
await foreach()
, que indica que cada elemento se obtendrá de forma asíncrona y, por supuesto, sin esperar a tener materializada la colección completa.
yield
conforme van siendo generados de forma asíncrona:await foreach (var i in GetNumbers())
{
Console.WriteLine(i);
}
async IAsyncEnumerable<int> GetNumbers()
{
for (var i = 0; i < 1000_000_000; i++)
{
var a = await Task.FromResult(i * 2); // <-- Esto podría ser cualquier tipo
yield return a; // de operación asíncrona
}
}
Otra de las ventajas de utilizar IAsyncEnumerable
es que en los métodos generadores podríamos utilizar tokens de cancelación, aunque para conseguirlo de forma correcta debemos hacer uso de dos particularidades:-
Primero, en la llamada al método generador, debemos utilizar el extensor
WithCancellation()
para suministrarle el token de cancelación al que deberá atender para cancelar el streaming de valores.
-
Segundo, en la signatura del método generador, debemos añadir un parámetro de tipo
CancellationToken
para recibir el token de cancelación, decorándolo con el atributo[EnumeratorCancellation]
.
public static async Task Main()
{
var token = new CancellationTokenSource(100); // Cancel after 100 ms
await foreach (var i in GetNumbers().WithCancellation(token.Token))
{
Console.WriteLine(i);
}
}
static async IAsyncEnumerable<int> GetNumbers(
[EnumeratorCancellation] CancellationToken token = default)
{
for (var i = 0; i < 1000_000_000 && !token.IsCancellationRequested; i++)
{
var a = await Task.FromResult(i * 2);
yield return a;
}
}
Y, ¿sirve esto para algo?
Sin duda, y probablemente cada vez más, puesto que casi todos los tipos de aplicación (web, desktop, móvil...) al final necesitan mejorar su capacidad de obtener datos de forma asíncrona desde cualquier tipo de origen (bases de datos, APIs, etc) y, además, mostrar datos al usuario de la forma más rápida posible, manteniendo al mínimo el consumo de recursos del servidor.Y dado que esta nueva característica abre la puerta a funcionalidades muy interesantes de intercambio de datos en streaming, ya podemos verla reflejada en frameworks como ASP.NET Core o SignalR.
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario