C# 8.0: Secuencias asíncronas

Antes que nada, aprovecho para felicitar al lector por la llegada del nuevo año. ¡Le deseo muchas cosas buenas en 2020!

Me permito esta vez alterar lo que sería un orden más natural de presentación de las nuevas características añadidas a C# 8.0 y saltar directamente a una de las más avanzadas, las secuencias asíncronas (async enumerables). Lo hago principalmente como modesto homenaje al cierre de MSDN Magazine, que en su ejemplar final incluye un excelente artículo dedicado al tema, «Iterating with Async Enumerables in C# 8» , de Stephen Toub, disponible aquí. En esta entrega intentaré hacer lo que siempre: no repetir, sino intentar una presentación didáctica y un ejemplo claro y sencillo de utilización de la tecnología en cuestión que complemente a los que pueden encontrarse en las fuentes oficiales. Para acceder a los detalles más técnicos, remito al lector al artículo antes mencionado o a la descripción online de la característica en docs.microsoft.com, que parte de aquí.

En esencia, las secuencias asíncronas hacen posible que un código cliente recorra los elementos de una secuencia utilizando llamadas asíncronas a la hora de producir los elementos de la misma, de modo que el hilo que lleva a cabo el recorrido (enumeración) no quede bloqueado mientras espera por un elemento y pueda llevar a cabo algún otro trabajo útil. Esta posibilidad adquiere una utilidad especial cuando la secuencia a recorrer no es estática o está generada de antemano, sino que se compone dinámicamente a partir de elementos que provienen de una fuente externa, como ocurre en el ejemplo que presentaremos a continuación.

Para hacer posible la implementación de las secuencias asíncronas, .NET Core 3.0 introduce las nuevas interfaces IAsyncEnumerable<T> e IAsyncEnumerable<T> en el espacio de nombres System.Collections.Generic, y la interfaz IAsyncDisposable en el espacio de nombres System. Estas interfaces guardan mucha semejanza con  sus homólogas en el mundo síncrono:

namespace System.Collections.Generic
{
    public interface IAsyncEnumerable<out T>
    {
        IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default);
    }

    public interface IAsyncEnumerator<out T>
    {
        ValueTask<bool> MoveNextAsync();
        T Current { get; }
    }
}

namespace System
{
    public interface IAsyncDisposable
    {
        ValueTask DisposeAsync();
    }
}

Observe que la interfaz de enumerador asíncrono no tiene equivalente para el método Reset() de IEnumerator, que ha sido marcado como obsoleto en .NET Core 3.0. Sobre ValueTask y ValueTask<T> ya hablamos en un artículo anterior.

Para producir una secuencia asíncrona en C# 8.0, basta con utilizar la interfaz asíncrona necesaria en la firma del método. Para consumirla, se debe utilizar la construcción await foreach en vez de foreach:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Test
{
    class Program
    {
        static async IAsyncEnumerable<int> FibonacciAsync()
        {
            int n0 = 1, n1 = 1;
            while (true)
            {
                await Task.Delay(100);
                int n2 = n0 + n1;
                n0 = n1; n1 = n2;
                yield return n2;
            }
        }
        static async Task Main(string[] args)
        {
            await foreach(int f in FibonacciAsync())
            {
                Console.WriteLine(f);
                if (f > 100)
                    break;
            }
        }
    }
}

Como ya nos tiene acostumbrados, el compilador se encarga de generar la máquina de estados necesaria para generar la asincronía, y el try/finally (llamando a DisposeAsync, claro) alrededor del recorrido de la secuencia. Puede encontrar más detalles técnicos sobre todo ello en el artículo de Stephen Toub antes mencionado.

Para el ejemplo especial de hoy, debo remitirme a un artículo que escribí para MSDN hace mucho tiempo (febrero de 2008, para ser más exactos). El artículo, llamado «Applying LINQ to new data types«, y que ya solo está disponible aquí (¡gracias a mis buenos amigos de Plain Concepts por ello!), muestra cómo encapsular en un método cliente la recepción de una secuencia de mensajes provenientes de una fuente de comunicación interprocesos conocida como canalización con nombre (named pipe). La solución allí presentada, que se apoya en una secuencia síncrona, podría no ser eficiente, dado que los mensajes podrían llegar a intervalos de tiempo variables, y entonces el hilo que ejecuta el recorrido estaría completamente bloqueado durante esos intervalos esperando la llegada de un mensaje. El uso de una secuencia asíncrona podría venir aquí como anillo al dedo.

La conversión del método que produce los mensajes en un método asíncrono es bastante directa, gracias a que a estas alturas .NET ya está bien preparado para este tipo de programación; en particular, la clase NamedPipeClientStream ofrece variantes asíncronas para la conexión y lectura:

public static async IAsyncEnumerable GetMessagesAsync(
    this NamedPipeClientStream pipeStream)
{
    await pipeStream.ConnectAsync();
    pipeStream.ReadMode = PipeTransmissionMode.Message;

    Decoder decoder = Encoding.UTF8.GetDecoder();

    const int BufferSize = 256;
    byte[] bytes = new byte[BufferSize];
    char[] chars = new char[BufferSize];
    int nBytes = 0;
    StringBuilder msg = new StringBuilder();
    do
    {
        msg.Length = 0;
        do
        {
            nBytes = await pipeStream.ReadAsync(bytes, 0, BufferSize);
            if (nBytes > 0)
            {
                int nChars = decoder.GetCharCount(bytes, 0, nBytes);
                decoder.GetChars(bytes, 0, nBytes, chars, 0, false);
                msg.Append(chars, 0, nChars);
            }
        } while (nBytes > 0 && !pipeStream.IsMessageComplete);
        decoder.Reset();
        if (nBytes > 0)
        {
            // we've got a message - yield it!
            yield return msg.ToString();
        }
    } while (nBytes != 0);
}

El consumo de la secuencia asíncrona por el código cliente es también bastante inmediato:

static async Task Main(string[] args)
{
    const string Server = ".";
    const string PipeName = "CS3";
 
    using (NamedPipeClientStream pipeStream =
      new NamedPipeClientStream(Server, PipeName, PipeDirection.InOut))
    {
        await foreach (var s in pipeStream.GetMessagesAsync())
            Console.WriteLine(s);
    }
    Console.ReadLine();
}

Hasta aquí todo muy bien, pero para poder ir más allá hay un gran PERO: como bien explica Stephen Toub al final de su artículo, ninguno de los dos mecanismos de consultas integradas (LINQ) que ofrecen .NET Core 3.0 y C# 8.0 (los métodos extensores y la sintaxis de consulta) incluye soporte alguno para las secuencias asíncronas; para ello, por ahora, hay que acudir a la librería System.Linq.Async del proyecto github.com/dotnet/reactive. Así que le debo para la próxima, estimado lector, una reescritura del último ejemplo de mi artículo sobre las canalizaciones, en el que filtraba y ordenaba los mensajes recibidos mediante una consulta LINQ. ¡Hasta entonces!


Advertencia: Como casi siempre hago, comencé probando el código presentado aquí en mi Mac, al que tengo mucho apego. Viendo que las canalizaciones con nombre estaban soportadas en .NET Core 3.0, pensé que todo iría de maravillas; sin embargo, al intentar ejecutar el servidor obtuve el error «Message transmission mode is not supported on this platform«. Entonces tuve que cambiarme a Windows :-(.

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *