Ir al contenido principal

Programación asíncrona con async/await en .Net

Introducción A partir de la versión 4.5 de .Net framework se ha simplificado de forma considerable la forma en que podemos trabajar con código asíncrono. Con los anteriores frameworks, si queríamos contar con los beneficios de una programación asíncrona nos veíamos obligados a lidiar con una gran complejidad en nuestro código. Esto nos hacía evitar su uso en lo posible a pesar de perder esta importante característica. Stephen Cleary , un MVP especializado en concurrencia define la programación asíncrona como: Una forma de concurrencia que utiliza futuros o "callbacks" para evitar hilos innecesarios. La programación moderna con async y await nos abstrae de la utilización de "callbacks" y nos permite utilizar futuros (Tasks) que se encargarán de notificar al llamante cuando el método asíncrono se complete. async y await Las palabras async y await son las palabras clave que .Net ha introducido en el lenguaje para que podamos implementar métodos asíncronos co...

Inicialización diferida

Introducción

La inicialización o carga diferida (lazy initialization) nos permite posponer la creación de un objeto hasta su primer uso. Esto nos ayuda a aumentar el rendimiento de nuestras aplicaciones cuando tenemos objetos muy pesados que no son necesarios de forma inmediata e incluso puede que no se lleguen a usar nunca. También nos permite mejorar el inicio de un programa priorizando la carga instantánea de los objetos necesarios al inicio y posponiendo la carga de otros objetos pesados después.

Una forma muy simple de realizar una inicialización diferida sobre una propiedad es la siguiente: La variable de respaldo _pedidos se mantendrá sin inicializar hasta que accedamos a la propiedad Pedidos por primera vez. En ese momento se creará la instancia llamando al constructor. Las siguientes veces que accedamos a la propiedad obtendremos la misma instancia.

El problema que tenemos con este código es que no implementa seguridad en hilos (no es thread safe). Si estamos seguros que sólo vamos a acceder al objeto desde un mismo hilo podemos utilizar sin problemas este método. En caso contrario nos podríamos encontrar con varios accesos concurrentes que nos crearían varias instancias.

Para evitarlo tendríamos que realizar un bloqueo que nos garantizase que no se vayan a crear varias instancias desde distintos hilos.

Objeto Lazy<T>

Desde el Framework 4.0 de .Net contamos con un objeto muy útil para la inicialización diferida. Se trata del objeto Lazy<T>. Simplificando se trata de un objeto que envuelve una instancia de un objeto de tipo T en su propieadad Value. Inicialmente la instancia será nula, y cuando se acceda a ella por primera vez, se creará la instancia del objeto en memoria. La forma en que se cree la instancia del objeto depende de cómo hayamos creado la instancia de nuestro objeto Lazy:
  • Si nuestro objeto cuenta con un constructor por defecto sin parámetros podemos crear la instancia del objeto Lazy de forma sencilla:
  • En este caso Lazy creará la instancia mediante el uso del método Activator.CreateInstance.
  • Si nuestro objeto requiere de parámetros para su construcción o queremos utilizar nuestro propio método Factory deberemos usar la sobrecarga que acepta un delegado de tipo Func<T>:
  • Para este segundo caso, Lazy invocará el delegado suministrado para crear la instancia.
En ambos casos hemos creado la instancia del objeto Lazy que actuará de contenedor de nuestro objeto, pero el objeto todavía no se ha instanciado. Para instanciar el objeto debemos acceder a él mediante la propiedad Value del objeto Lazy. Al acceder a esta propiedad por primera vez se creará la instancia de nuestro objeto y se devolverá. A partir de entonces, cada vez que se acceda a esta propiedad se devolverá esta misma instancia.

Veamos ahora un ejemplo de incialización diferida de una propiedad con el objeto Lazy. La clase Cliente inicializa en su constructor un objeto Lazy para que se encargue de diferir la inicialización de sus Pedidos: Al acceder a la propiedad MisPedidos, como internamente se lee la propiedad Value de nuestro objeto Lazy, se desencadenará la inicialización de la instancia diferida: Si el contexto donde se use la instancia de Cliente no necesita acceder a los pedidos del cliente no se inicializarán nunca. Si estos pedidos tienen que ser obtenidos de una base de datos o un servicio externo estaremos ahorrando una carga de datos remota y además estaremos contribuyendo a un ahorro de memoria.

La mejora que obtenemos con respecto al primer método es que el objeto Lazy implementa seguridad en hilos por defecto. Si utilizamos cualquiera de las dos sobrecargas anteriores para crearlo, garantizamos que son "thread-safe", lo cual significa que varios consumidores del objeto podrían concurrir a solicitar la instancia pero sólo el primero de ellos crearía realmente la instancia, y los siguientes usarían esa misma instancia.

¿Y si no queremos seguridad en hilos?

Se pueden usar otras sobrecargas del constructor del objeto Lazy donde podemos establecer el parámetro isThreadSafe en false para mejorar el rendimiento en caso de que no utilicemos varios subprocesos. No hay que confundir entre la seguridad en la inicialización de la instancia con el posterior uso que se le de a esa instancia. Según msdn Microsoft: Haciendo el objeto Lazy<T> seguro para subprocesos no protege al objeto inicializado en diferido. Si hay multiples hilos que pueden acceder al objeto inicializado en diferido, debes hacer sus propiedades y métodos seguros para subprocesos.

Aplicado al patrón Singleton

El patrón Singleton restringe la instanciación de una clase a un sólo objeto y ofrece un único punto de acceso a esta instancia. La versión más simple del patrón singleton sería la siguiente: Se basa en restringir el acceso a la instancia únicamente por medio de su propiedad pública Instance, la cual crea la instancia la primera vez y devuelve la misma en sucesivos accesos. Al hacer privado el constructor impedimos que exista otra forma de crear la instancia. Otra característica de una clase Singleton sería la instanciación en diferido ya que mientras no se inicialice la clase no se creará la instancia del objeto.

Aquí nos volvemos a encontrar con el problema de entornos multihilo, ya que podríamos tener dos hilos en ejecución que evaluasen la condición instance == null como true. En este caso, ambos hilos crearían una instancia, violando así el principio del patrón Singleton que restringe a una única instancia.

Esto nos llevaría a utilizar técnicas de bloqueo usando un bloque lock cada vez que accedamos a la instancia, lo que podría repercutir en el rendimiento de nuestra aplicación. También podríamos recurrir al uso de constructores estáticos para forzar que sólo se invoque una vez. Esto simplificaría bastante la lógica de bloqueos pero bastaría con tener otro miembro estático para perder la garantía de que la instancia se crea de forma diferida. Para profundizar más sobre los distintos tipos de Singleton posibles y sus pros y contras aconsejo leer este magnífico artículo de http://csharpindepth.com/.

Una solución muy elegante que nos asegura la seguridad en subprocesos y la inicialización en diferido es utilizando de nuevo el objeto Lazy. El Singleton resultante es muy simple y tiene muy buen rendimiento:

Inicialización diferida en los sistemas ORM

En sistemas ORMs como Entity Framework o Hibernate se puede utilizar la carga diferida en sus propiedades de navegación. De esta manera podemos trabajar con una entidad "padre" sin necesidad de cargar en memoria todos sus miembros de otras entidades o de colecciones de entidades "hijas" mientras no sean requeridas. Si en algún momento accedemos a uno de estos miembros se inicializarán , se realizará la consulta a la base de datos y se mapearán sus propiedades con los datos recibidos. Para el caso de las colecciones hay que destacar que no es la creación del objeto colección el que se pospone, ya que se crea en el propio constructor de la clase "padre", sino los elementos que forman parte de la colección. En definitiva es en los elementos donde reside toda la carga de memoria en una colección.

No sólo mejoramos el rendimiento en términos de procesamiento y de memoria de la aplicación sino que mejoramos el tráfico entre la aplicación y la base de datos e incluso haremos que las consultas ejecutadas en la base de datos sean más ligeras ya que eliminaremos los joins con otras tablas.

Imaginemos que queremos obtener de la base de datos el nombre y la dirección de un cliente. Aunque la entidad de un cliente tenga una propiedad de navegación a una colección de sus pedidos no nos interesa obtenerlos porque nos penalizaría su carga. Por esta razón, los ORMs suelen tener carga en diferido automática por defecto en sus propiedades de navegación.

Aunque si nos interesa podemos hacer que la carga sea instantánea en lugar de diferida. En Entity Framework por ejemplo esto se hace mediante el método Include del contexto y pasando un string que represente la relación que queremos cargar.

También podemos desactivarla y forzar una carga explícita, es decir, que tengamos que cargarla nosotros mediante una llamada explícita.

A continuación tenemos los tres ejemplos de cargas posibles en Entity Framework:
  1. Carga diferida. Con la carga diferida activada, primero cargaremos los clientes pero dejaremos la propiedad de navegación de sus pedidos vacía. Sólo cuando accedamos a sus pedidos será cuando se realice la carga.
  2. Carga explícita. Aun con la carga diferida desactivada podremos realizar una carga en diferido, eso sí, lo tendremos que hacer de forma explícita. En el ejemplo vemos dos tipos de carga explícita sobre propiedades de navegación de una entidad. La primera de ellas se ha utilizado el método Reference por tratarse de una entidad simple. Para la segunda propiedad de navegación se ha utilizado el método Collection por tratarse de una colección de entidades.
  3. Carga instantánea. Especificando el "path" de nuestra relación estaremos realizando en una misma carga todas las entidades implicadas en la relación. En el ejemplo estamos realizando la carga de un cliente, todos sus pedidos y los detalles de los mismos.

Resolución de instancias en diferido con Unity

Aunque estemos utilizando un contenedor DI como Unity podemos hacer que nuestras instancias se creen también en diferido. El registro de nuestro tipo en el contenedor lo haremos como siempre. En el momento que queramos resolver el tipo utilizaremos el objeto Lazy<T>, donde T será el tipo que hemos registrado.

Comentarios

Entradas populares de este blog

La importancia del encapsulamiento. Parte 1. Introducción.

Introducción En programación orientada a objetos el término encapsulamiento es utilizado indistintamente para describir dos conceptos diferentes pero a la vez relacionados entre sí: Como mecanismo de restricción del acceso a componentes de un objeto; Como construcción del lenguaje para facilitar el “empaquetado” del estado y el comportamiento. En este post el significado al que se hace referencia es al primero de ellos. La visibilidad o accesibilidad es el primer paradigma que nos encontramos al empezar a escribir nuestro código. Cuando comenzamos a escribir una clase, interfaz, enumerado o estructura, lo primero que hacemos es establecer su modificador de acceso, y si no lo hacemos, se establecerá el modificador por defecto. Lo mismo ocurre cuando empezamos a escribir cada uno de sus miembros: variables de estado (campos) y métodos: lo primero de todo es escribir su modificador de acceso. A menudo me encuentro con código sin tener en cuenta este aspecto: código demasiado “ab...

La importancia del encapsulamiento. Parte 2. Un ejemplo de encapsulamiento en la vida real.

Un ejemplo de encapsulamiento en la vida real Supongamos un taller de carpintería que fabrica muebles a medida. Por una parte tendríamos el propio taller, que sería la clase a encapsular. Por otra los clientes, que serían nuestros usuarios y representarían las clases que USAN la clase taller. Y por otra parte podríamos pensar en talleres filiales que representarían una especialización del taller matriz, es decir, clases que extienden al taller de carpintería y por lo tanto SON talleres. El taller cuenta con funcionalidades puestas al servicio de los clientes como la elaboración de presupuestos, consulta de catálogos, selección de tipo de madera, selección de color, etc. También cuenta con funcionalidades propias del taller como cortar, lijar, pintar, barnizar, ensamblar mueble, etc. Para la elaboración de los presupuestos el taller matriz cuenta con una tarifa de precios que sólo los talleres pueden acceder para poder elaborar los presupuestos. Esta tarifa la fija el taller mat...

Programación asíncrona con async/await en .Net

Introducción A partir de la versión 4.5 de .Net framework se ha simplificado de forma considerable la forma en que podemos trabajar con código asíncrono. Con los anteriores frameworks, si queríamos contar con los beneficios de una programación asíncrona nos veíamos obligados a lidiar con una gran complejidad en nuestro código. Esto nos hacía evitar su uso en lo posible a pesar de perder esta importante característica. Stephen Cleary , un MVP especializado en concurrencia define la programación asíncrona como: Una forma de concurrencia que utiliza futuros o "callbacks" para evitar hilos innecesarios. La programación moderna con async y await nos abstrae de la utilización de "callbacks" y nos permite utilizar futuros (Tasks) que se encargarán de notificar al llamante cuando el método asíncrono se complete. async y await Las palabras async y await son las palabras clave que .Net ha introducido en el lenguaje para que podamos implementar métodos asíncronos co...