0

.Net Asincrónico: Patrones

La programación asincrónica nos permite realizar varias operaciones en paralelo al mismo tiempo sin la necesidad de esperar que se den como finalizadas. Por otro lado, no bloquean al programa permitiendo continuar mientras las tareas se ejecuten, aunque sean de larga duración.

En .Net tenemos la implementación de los siguientes patrones asincrónicos:

  • APM, Asynchronous Programming Model.
  • EAP, Event- Based Asynchronous Pattern.
  • TAP, Task-Based Asynchronous Pattern.

Veamos cada uno en detalle.

APM, Asynchronous Programming Model

Este es uno de los primeros que fueron implementados en .Net. Existe de la versión de .Net Framework 1.1. Este usa la interfaz IAsyncResult para su implementación. Esta interfaz nos brinda un método que inicia una operación asincrónica y otro que concluye la operación mediante un parámetro.

Supongamos que vamos a implementar una operación que llamaremos OperationName. Al usar la interfaz tendremos 2 métodos: BeginOperationName() y EndOperationName() que serán inicio y final correspondiente. Co BeginOperationName() iniciamos la operación asincrónica y devolveremos el valor representado de la interfaz. También podemos pasar parámetros o pasar un estado de contexto o una dependencia.

Vamos a ver el ejemplo más típico de implementación con FileStream que perteneces a System.IO. Leeremos un archivo. Tendremos 2 métodos, BeginRead() y EndRead() asincrónicos para leer los bytes del archivo:

public class FileReader
{
    private byte[]? _buffer;
    private const int InputReportLength = 1024;
    private FileStream? _fileStream;

    public void BeginReadAsync()
    {
        _buffer = new byte[InputReportLength];
        _fileStream = File.OpenRead("User.txt");
        _fileStream.BeginRead(_buffer, 0, InputReportLength, ReadCallbackAsync, _buffer);
    }
    public void ReadCallbackAsync(IAsyncResult iResult)
    {
        _fileStream?.EndRead(iResult);
        var buffer = iResult.AsyncState as byte[];
    }
}

BeginRead() acepta un parámetro opcional que no da soporte para un AsyncCallback. Por esta razón definimos un método BeginReadAsync() que nos permitirá para el método declarado más abajo. De esta manera tenemos disponible la lectura de un archivo de manera asincrónica.

EAP, Event- Based Asynchronous Pattern

Como su nombre lo indica, usaremos eventos para implementar las operaciones. Todas las operaciones se ejecutarán en subprocesos por separado y utilizaremos eventos para sincronizar y notificar el estado final de este. Muchos de los componentes de la Interfaz Gráfica implementan este tipo de patrón permitiendo que los componentes, por ejemplo, se dibujen mientras realizan otras tareas comunes sin necesidad de bloquear el proceso principal.

El funcionamiento básicamente es un componente que se suscribe a un evento relacionado con un subproceso que notificará al componente cuando este finalice. Gracias a esto la interfaz de usuario no necesita esperar hasta que se completen todas las operaciones.

EAP puede usarse de muchas maneras, no solamente con las interfaces gráficas. Por esta razón, desde la versión de .Net Framework tenemos la implementación de este patrón disponible.

Para implementar EAP deberemos implementar una operación OperationNamedCompleted que notifica que la operación se completó. Lo siguiente es personalizar los parámetros para enviar OperationsNameEventsArgs donde definiremos las propiedades para almacenar los resultados de las operaciones y los estados de los eventos. Por último, creamos la operación OperationNameAsync() y le enviaremos los parámetros necesarios para ejecutar la operación asincrónica. 

Veamos un ejemplo más concreto. Podemos crear una clase Product con 2 propiedades: Id y Name:

public class Product
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

Ahora crearemos un servicio que simulara una ejecución que lleve bastante tiempo:

public class ProductService
{
    private readonly List<Product> _users = new List<Product>
    {
        new Product{ Id = 1, Name = "Computer"},
        new Product{ Id = 2, Name = "Television"}
    };
    public Product GetProduct(int productId)
    {
        // Long-running operation
        return _users.FirstOrDefault(x => x.Id == productId);
    }
}

Lo siguiente, los argumentos:

public class GetProductCompletedEventArgs : AsyncCompletedEventArgs
{
    private Product _result;
    public Product Result
    {
        get
        {
            RaiseExceptionIfNecessary();
            return _result;
        }
    }
    public GetProductCompletedEventArgs(Exception error, bool cancelled, Product Product)
        : base(error, cancelled, Product)
    {
        _result = Product;
    }
}

La implementación final:

public class EapProductProvider
{
    private readonly SendOrPostCallback _operationFinished;
    private readonly ProductService _ProductService;
    public EapProductProvider()
    {
        _operationFinished = ProcessOperationFinished;
        _ProductService = new ProductService();
    }
    public Product GetProduct(int ProductId) => _ProductService.GetProduct(ProductId);
    public event EventHandler<GetProductCompletedEventArgs> GetProductCompleted;
    public void GetProductAsync(int ProductId) => GetProductAsync(ProductId, null);
    private void ProcessOperationFinished(object state)
    {
        var args = (GetProductCompletedEventArgs)state;
        GetProductCompleted?.Invoke(this, args);
    }
}

_operationFinished es un campo delegado de SendOrPostCalolback que representa un método de devolución de llamada que queremos ejecutar cuando un mensaje se notifica e un contexto sincrónico. La propiedad GetProductCompleted representa el evento que se desencadena al finalizar la operación. Permite la suscripción a esta operación. Ahora agregaremos un método GetProductAsync() al provider:

public void GetProductAsync(int productId, object productState)
{
    AsyncOperation operation = AsyncOperationManager.CreateOperation(productState);
    ThreadPool.QueueProductWorkItem(state =>
    {
        GetProductCompletedEventArgs args;
        try
        {
            var Product = GetProduct(productId);
            args = new GetProductCompletedEventArgs(null, false, Product);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
            args = new GetProductCompletedEventArgs(e, false, null);
        }
        operation.PostOperationCompleted(_operationFinished, args);
    }, productState);
}

Creamos AsyncOperation para realizar un seguimiento e informar del avance de la tarea asincrónica. Luego, ejecutamos la operación de forma asincrónica en un grupo de subprocesos utilizando ThreadPool.QueueWorkItem(). Lo siguiente, notificamos la finalización de la operación mediante la invocación PostOperationCompleted() desde AsyncOperation y se dispara GetUserCompleted. Ahora crearemos EventBasedAsyncPatterHelper y agregaremos el metodo GetchAndPrintUser.

public static class EventBasedAsyncPatternHelper
{
    public static void FetchAndPrintProduct(int productId)
    {
        var eapUserProvider = new EapProductProvider();
        eapUserProvider.GetProductCompleted += (sender, args) =>
        {
            var result = args.Result;
            Console.WriteLine($"Id: {result.Id}\nName: {result.Name}");
        };
        eapUserProvider.GetProductAsync(productId);
    }
}

Por último, llamamos al método FetchAndPrintProduct() para recuperar e imprimir la información de un producto de forma asincrónica.

EventBasedAsyncPatternHelper.FetchAndPrintUser(1);

TAP, Task-Based Asynchronous Pattern.

Este patrón está implementado desde la versión 4 de .Net Framework. Este patrón es el más recomendado para implementar operaciones asíncronas cuando estás comenzando. como desarrollador. 

En c# tenemos 2 palabras reservadas: async y await que nos ayudan a implementar el patrón. Con esto le indicamos al compilador que debe crear la máquina de estados correspondiente para atender las ejecuciones de las llamadas asíncronas. La palabra clave await para la ejecución de nuestras operaciones hasta que una tarea termine o esté completa.

Veamos la implementación:

public Task<int> Operation1Async(int param)
{
    // more code
    return Task.FromResult(1);
}
public async Task<int> Operation2Async(int param)
{
    // more code with await
    return 1;
}

Podemos ver que el metodo devuelve un Task<T>. En Operation1 devuelve un objeto Task que encapsula el ciclo de vida asincronico y debemos administrarlo manualmente para crear y ejecutar el cierre de la tarea.

En Operation2 usamos async. Es mucho más sencillo de utilizar, el compilador se encargará de generar todo el manejo de objeto Task junto a todo su ciclo de vida solo necesitaremos controlar el flujo con el Async y Await.

Estas operaciones pueden ser canceladas en cualquier momento. Supongamos que hacemos una solicitud y que la operación tomará un tiempo. Si el usuario no quiere esperar o se arrepiente de la solicitud puede cancelar la operación para liberar recursos.

Para lograr la cancelación de una tarea utilizaremos CacellationToken. En pocas palabras, es el camino por el cual podemos cancelar la solicitud relacionada con Task. Es un parámetro opcional, pero es una buena práctica utilizarlo. Al utilizarlo la operación estará monitoreando que hay alguna cancelación.

var cancelToken = new CancellationTokenSource();
Task.Factory.StartNew(async () =>
{
    await Task.Delay(3000, cancelToken.Token);
    // API call
}, cancelToken.Token);
//Stops the task
cancelToken.Cancel(false);

El método cancel es el que le notificará la cancelación de la solicitud que hemos realizado a todas las tareas relacionadas.

Conclusiones

Hemos visto estos 3 patrones que están disponibles en .Net para programación asincrónica. Seguramente el último es el que más has utilizado ya que casi es obligatorio usarlo para realizar una buena práctica en el uso de recursos. Espero que les haya gustado el post.

Fernando Sonego

Deja una respuesta

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