Asincronismo en .Net

La programación asincrónica nos  brinda la oportunidad de  llevar a cabo múltiples operaciones simultáneamente, sin la obligación de aguardar a que todas se completen. Además, no bloquea la ejecución del programa, permitiendo que este continúe su flujo incluso cuando se trata de tareas prolongadas.

En .Net tenemos la implementación de los siguiente patrones asincronicos:

  • 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 se implementaron en .NET, presente desde la versión 1.1 de .NET Framework. Utiliza la interfaz IAsyncResult para su implementación, la cual proporciona un método para iniciar una operación asincrónica y otro para finalizar, mediante un parámetro.

Supongamos que estamos a punto de implementar una operación que llamaremos “OperationName”. Al utilizar la interfaz, contaremos con dos métodos: BeginOperationName() y EndOperationName(), encargados de gestionar el inicio y el final de la operación, respectivamente. Con BeginOperationName(), iniciamos la operación asincrónica y devolvemos el valor representado por la interfaz. Además, tenemos la opción de pasar parámetros, un estado de contexto o alguna dependencia necesaria durante el proceso.

Vamos a explorar el ejemplo más común de implementación utilizando FileStream, que pertenece al espacio de nombres System.IO. En este caso, estaremos leyendo un archivo. Contaremos con dos métodos asincrónicos, BeginRead() y EndRead(), encargados de realizar la lectura de 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 sugiere su nombre, emplearemos eventos para llevar a cabo la implementación de las operaciones. Cada operación se ejecutará en subprocesos independientes, utilizando eventos para sincronizar y notificar el estado final. Este patrón es ampliamente utilizado en muchos componentes de la Interfaz Gráfica, permitiendo, por ejemplo, que dichos componentes se dibujen mientras realizan otras tareas comunes sin necesidad de bloquear el proceso principal.

El funcionamiento esencialmente implica que un componente se suscribe a un evento vinculado a un subproceso que notificará al componente cuando este finalice. Gracias a esto, la interfaz de usuario no necesita aguardar hasta que todas las operaciones se completen.

La Programación Asíncrona basada en Eventos (EAP) puede aplicarse de diversas maneras, no limitándose exclusivamente a las interfaces gráficas. Por esta razón, contamos con la implementación de este patrón disponible desde la versión de .NET Framework.

Para implementar EAP, será necesario incorporar una operación llamada OperationNamedCompleted, encargada de notificar la finalización de la operación. Posteriormente, personalizamos los parámetros para enviar OperationsNameEventArgs, donde definiremos propiedades para almacenar los resultados de las operaciones y los estados de los eventos. Finalmente, creamos la operación OperationNameAsync() y le proporcionamos 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 ha sido implementado desde la versión 4 de .NET Framework y es altamente recomendado para aquellos que están iniciando en el desarrollo. 

En C#, contamos con dos palabras reservadas clave: async y await, las cuales facilitan la implementación de este patrón. Al utilizarlas, le indicamos al compilador que debe generar la máquina de estados correspondiente para gestionar las ejecuciones de las llamadas asíncronas. La palabra clave await se utiliza para pausar la ejecución de nuestras operaciones hasta que una tarea haya concluido 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 explorado estos tres patrones disponibles en .NET para la programación asincrónica. Es probable que el último, en particular, sea el que más hayas empleado, ya que su uso es casi obligatorio para implementar buenas prácticas en la gestión eficiente de recursos. Espero que hayan disfrutado del contenido de este post.

Si desean conocer más sobre estos patrones o tienen alguna pregunta adicional, no duden en compartir sus comentarios. ¡Gracias por seguirnos!

0 0 votos
Valora la Publicación
Suscribirse
Notificación de
guest
0 Comentarios
Feedback en línea
Ver todos los Comentarios

Comentarios Recientes

0
Nos encantaría conocer tu opinión: ¡comenta!x
Ir a la barra de herramientas