C# 8.0 – Specification – Asynchronous Streams
Índice general – C# 8.0 – Specification
C# como tal, tenía la posibilidad de iterar un método y ejecutar un método como asíncrono, pero no contemplaba la posibilidad de que el método fuera asíncrono y se pueda iterar al mismo tiempo.
Para lograr este propósito, Microsoft ha tenido que cambiar la forma en la que la interfaz IAsyncEnumerable<T> trabaja teniendo en cuenta estas circunstancias.
Un ejemplo de iterar un método asíncrono de forma tradicional (no recomendada) sería por ejemplo:
private static async Task UseTraditionalAsyncStreamsAsync() { var watch = System.Diagnostics.Stopwatch.StartNew(); foreach (var data in await GetTraditionalDataAsync()) { Console.WriteLine($"{nameof(UseTraditionalAsyncStreamsAsync)} - {data} - {watch.ElapsedMilliseconds}"); } watch.Stop(); var elapsedMilliseconds = watch.ElapsedMilliseconds; Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"{nameof(UseTraditionalAsyncStreamsAsync)} - {elapsedMilliseconds}"); Console.ForegroundColor = ConsoleColor.Green; } private static async Task<IEnumerable> GetTraditionalDataAsync() { var data = new List(); for (var i = 1; i <= 5; i++) { // Simulate a delay for show how it works. await Task.Delay(350); data.Add(i); } return data; }
Al llamar a UseTraditionalAsyncStreamsAsync, tenemos que la duración del proceso es de aproximadamente 350 milisegundos por iteración (tiempo forzado para demostrar alguna de las diferencias que veremos a continuación), por lo que en 5 iteraciones se va a cerca de 1.8 segundos.
La particularidad de este tratamiento es que la salida no la obtenemos hasta que el último elemento de la iteración finaliza, por lo que si tuviéramos que operar con los elementos devueltos en cada iteración, no podríamos hacer nada hasta que finalizara el trabajo de dicha iteración.
Ejecutando este código, obtendremos un resultado parecido al siguiente:
UseTraditionalAsyncStreamsAsync - 1 - 1848 UseTraditionalAsyncStreamsAsync - 2 - 1849 UseTraditionalAsyncStreamsAsync - 3 - 1849 UseTraditionalAsyncStreamsAsync - 4 - 1849 UseTraditionalAsyncStreamsAsync - 5 - 1849 UseTraditionalAsyncStreamsAsync - 1849ms
La duración del proceso es el lógico debido al Delay forzado.
Sin embargo, podemos apreciar que cada una de las iteraciones no finaliza hasta que finaliza todo el hilo de ejecución completamente.
Y aquí, es donde entra en juego Asynchronous Streams, el uso de IAsyncEnumerable<T> y de concremente yield.
El código que hemos visto anteriormente, quedaría de la siguiente forma:
private static async Task UseAsyncStreamsAsync() { var watch = System.Diagnostics.Stopwatch.StartNew(); await foreach (var data in GetDataAsync()) { Console.WriteLine($"{nameof(UseAsyncStreamsAsync)} - {data} - {watch.ElapsedMilliseconds}"); } watch.Stop(); var elapsedMilliseconds = watch.ElapsedMilliseconds; Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"{nameof(UseAsyncStreamsAsync)} - {elapsedMilliseconds}ms"); Console.ForegroundColor = ConsoleColor.Green; } private static async IAsyncEnumerable GetDataAsync() { for (var i = 1; i <= 5; i++) { // Simulate a delay for show how it works. await Task.Delay(350); yield return i; } }
En este caso, el resultado que obtendríamos sería el siguiente:
UseAsyncStreamsAsync - 1 - 366 UseAsyncStreamsAsync - 2 - 727 UseAsyncStreamsAsync - 3 - 1085 UseAsyncStreamsAsync - 4 - 1445 UseAsyncStreamsAsync - 5 - 1819 UseAsyncStreamsAsync - 1820ms
Aquí ya podemos apreciar diferencias notables.
Como podemos deducir, la «no» diferencia es el tiempo que tarda debido al Delay, algo lógico y razonable.
Pero esto no tiene nada que ver con Asynchronous Streams.
Lo que sí tiene que ver es que cada elemento está siendo devuelto en tiempo tal y como esperaríamos que ocurriera en la vida real, por lo que no necesitamos esperar a que termine el proceso completo de iteración para poder actuar sobre el proceso completo.
Dentro del código de nuestro foreach vemos que utilizamos await delante del bucle.
El método que itera es de tipo IAsyncEnumerable<int>.
Y dentro de este método utilizamos yield para ir devolviendo los resultados en la iteración.
El bucle foreach va recibiendo cada dato, y en nuestro caso, lo va mostrando uno a uno según va finalizando cada iteración.
Una aplicación muy útil en estos casos son procesos que requieren el tratamiento rápido de información, ya sea por datos devueltos por una sonda o sensor, o por datos que deben ser gestionados de forma rápida, y eso sin tener en cuenta consumo de CPU, bloqueo de procesos, memoria RAM o procesador.
Happy Coding!
One Responseso far
Buen comentario de esta función Álvaro siguió de cerca tus publicaciones tocayo