domingo, 20 de octubre de 2013

Liberación de Recursos y Finalizadores en C#

Tabla de Contenido

0. Introducción
1. Finalizadores en C#
1.1 El Método Object.Finalize
1.1.1 Limitaciones del método Finalize
2. La Clase Estática GC
3. Implementación del Patrón Desechable con IDisposable
4. Conclusiones
5. Glosario
6. Enlaces & Literatura

0. Introducción

El programador en algún su momento de su profesión se enfrentará a situaciones que requieren la implementación o uso de recursos del sistema operativo: manipulación de archivos, manejo de recursos visuales (ventanas), o el establecimiento de una conexión de red, entre muchos otros. Veremos que es necesario entender el concepto de gestión automática de recursos que provee la máquina virtual -CLR- de C#; exactamente el manejo de memoria automática para el uso eficiente de los recursos; además, es relevante antes de empezar que C# provee un mecanismo que nos apropia de esta gestión para controlar y manejar adecuadamente el uso de recursos no gestionados.

1. Finalizadores en C#

Los finalizadores son miembros de clase que entran en juego (es decir, se ejecutan) antes de que el recolector de basura (garbage collector, en inglés) inicie su rutina de descubrimiento o búsqueda de objetos no referenciados y los marque como candidatos a ser eliminados.

En C# la declaración sigue este sencillo esquema:

Archivo C# ConexionBD.cs [enlace alternativo]:
Obsérvese que basta con anteponer el símbolo ~ (símbolo crema) al nombre del tipo, como en: ~Conexion (). Y en el cuerpo se ha de especificar el grupo lógico de sentencias para finalizar o reclamar los recursos usados por el tipo de objeto.

La CLR se encarga de la gestión automática de memoria. Sin embargo, hay escenarios donde nos enfrentamos al uso de recursos que a ciencia cierta el garbage collector desconoce sobre cómo eliminarlos. A este tipo de recursos los llamamos no gestionados (unmanaged resources), y comprenden: manipulación de archivos, gestión de GUI (i.e.: ventanas), establecimiento de una conexión a un recurso remoto (i.e.: un archivo), etc.

Todos estos tipos recursos enlistados son adyacentes al sistema operativo. .NET introduce un mecanismo de liberación de recursos no gestionados a través de un método especial común a todos las clases de objetos predefinidos y personalizados: Object.Finalize(), que será el programador quien tenga el poder de implementar la lógica necesaria para llegar a cabo la liberación de los recursos usados.

1.1 El Método Object.Finalize

Implícitamente este método se encarga de realizar tareas de limpieza sobre recursos no gestionados antes de que el garbage collector se encargue de destruir la instancia que tiene asociada todos los recursos usados en el ciclo de vida del objeto. Se dice que es implícita, porque es ejecutado de manera automática por la CLR cuando la instancia de clase ya no es accesible.

En línea con lo anterior, si el objeto ha sido excluído a través del método SuppressFinalize [4] (clase estática GC [5] en namespace System), entonces no se invocará este método (Finalize, desde luego). Sin embargo, Finalize se invocará cuando la aplicación finalice su ejecución sobre aquellos que aún se encuentren accesibles y no eximidos de finalización.

La sintaxis de definición de este método en C# [3] es:

protected virtual void Finalize()

La firma de este método evidencia que se trata de un recurso heredable pero no accesible desde código cliente.

A través de la nota expuesta en [3] nos dicen:
Acerca de la implementación del método finalize.
Figura 1. Nota acerca de la implementación del método Finalize [3].

1.1.1 Limitaciones del método Finalize

De acuerdo con [3], el método Finalize posee las siguientes limitaciones en C#:
  • Se desconoce el momento en que la CLR invoca este método. Es decir, que los recursos no se liberarán en un momento específico. Puede ocurrir lo contrario si se hace una llamada a cualquiera de los métodos Close (suspensión del recurso con posible reuso futuro en el ciclo de vida, por ejemplo en una conexión de base de datos) o Dispose [6] (implementación concreta del contrato de IDisposable [7]).
  • El orden de finalización de un conjunto de objetos no es determinístico; inclusive cuando existe una relación de dependencia referencial entre dos objetos.
  • No hay un hilo (en inglés, thread) de ejecución para el método Finalize asociado.
[Nota: En [8] se aclara con más detalle la diferencia entre Close and Dispose desde la perspectiva de conexiones a bases de datos.]

1.1.2 Ejemplo de uso de Finalize en C#

Recordemos que la llamadas al método Finalize ocurren desde el destructor; es por eso que en el siguiente ejemplo se implementa uno de estos para demostrar en qué momento sucede la destrucción del objeto.

Es de notar que en las líneas 19 a 24, se implementa el miembro destructor ~Contador (). El cual está encargado de llamar de forma implicita al método Finalize de la clase padre. Aunque pudimos haber sobrescrito este método, por motivos de simplicidad se ha ignorado.

En la Figura 2 se muestra una prueba de ejecución de esta implementación.
Ejecución de Contador.
Figura 2. Demostración de la inicialización y finalización de un objeto Contador.

2. La Clase Estática GC

En [5] explicitan la definición de esta clase así:
Controls the system garbage collector, a service that automatically reclaims unused memory.
Entonces podemos decir que esta clase estática provee al programador la posibilidad de controlar la finalización o recuperación de los recursos utilizados por las instancias creadas, pero que ya no están en uso.


Adicionalmente, la recolección de basura consiste en los 3 pasos siguientes [5]:
  1. El recolector de basura busca por objetos gestionados en las referencias creadas.
  2. El recolector de basura intenta finalizar a aquellos objetos no referenciados.
  3. El recolector de basura reclama la memoria de los recursos liberados.
Entrando en más detalles con la clase GC, ésta posee el método SuppressFinalize [4], con firma:

public static void SuppressFinalize (Object obj)

Este metodo permite decirle al recolector de basura que excluya la instancia pasada como argumento de invocar su finalizador asociado. Internamente, la CLR generará esta exclusión marcando un valor bandera (o bit) para evitar la eliminación del objeto de la memoria.

Un posible ejemplo del uso de este método se presenta a continuación:

Esta implementación es similar a la de clase Contador, sin embargo, en la línea 14 -GC.SuppressFinalize (this);- se explicita que la instancias actuales de Contador serán omitidas del ciclo de recolección de basura (el que se describió arriba en los tres pasos).

Demostración de SuppressFinalize en ContadorIndestructible.
Figura 3. Demostración de SuppressFinalize en ContadorIndestructible.

Aparte, otro de los métodos sobresalientes de GC es Collect [10], con firma:


public static void Collect()


Este método estático permite forzar la recolección de basura. No obstante, debe ser usado con cuidado debido a que la recolección de basura es un proceso de altos costes de tiempo de procesador y espacio en memoria.

Quiero agregar la aclaración explicita encontrada en [10]:
All objects, regardless of how long they have been in memory, are considered for collection; however, objects that are referenced in managed code are not collected. Use this method to force the system to try to reclaim the maximum amount of available memory.
Para decir que independientemente de la existencia de un objeto (al menos que pertenezca a la categoría de código administrado) en memoria, también será marcado para recolección. También, que Collect forzará al sistema a reclamar la mayor cantidad de memoria disponible.

Adapto el ejemplo encontrado en [10] para demostración de la utilidad de este método en C#:

Archivo C# Coleccionable.cs [enlace alternativo]:

En las líneas 7-16 creamos hasta 1000 objetos Version [11] de prueba para cargar la memoria con objetos que no se usarán pero que sí ocuparan en espacio. En la línea 24 del código cliente se muestra la cantidad de memoria asignada a la aplicación actual, además se le pasa el argumento false no forzar la recolección. Ocurre lo contrario, en la la línea 28 con true como argumento para el método GetTotalMemory [12].


Cuando hayamos compilado y ejecutada esta clase, tendremos el resultado que se muestra en la Figura 4.
Ejecución de la clase Coleccionable.
Figura 4. Resultados de la ejecución de la clase Coleccionable.

3. Implementación del Patrón Desechable con IDisposable

Tradicionalmente para la manipulación de recursos no gestionados (unmanaged resources) implementamos lógica try..catch..finally;es decir, con el manejo de excepciones. A continuación un ejemplo básico:

// Recurso no gestionado que requiere la manipulación prográmtico
// para la asignación y liberación de recursos (espacio en memoria)
TextReader textReaderObj = null;

try
{
// asignación de recurso
textReaderObj = new StreamReader (@"Archivos\datoscuentas.txt");
// conjunto de sentencias de lógica de lectura del flujo creado
string lineaActual = textReaderObj.ReadToEnd ();
Console.WriteLine (lineaActual);
}
catch (Exception e)
{
// Lógica de implementación para el manejo de la excepción generada
}
finally
{
// liberación de recursos asignados al flujo
if (textReaderObj != null)
{
textReaderObj.Dispose ();
}
}

Análogamente, podemos hacer uso de using [13], construcción que permite al programador pensar sólo en la asignación de recursos; implícitamente esta construcción se encargará de liberar los recursos asignados y a los objetos en memoria. Véamoslo con un ejemplo:

// Recurso no gestionado que requiere la manipulación prográmtico
// para la asignación y liberación de recursos (espacio en memoria)
// VERSIÓN RECOMENDADA
TextReader textReaderObj = null;

try
{
using (txtReaderObj = new StreamReader(@"Archivos\datoscuentas.txt"))
{
// conjunto de sentencias de lógica de lectura del flujo creado
string lineaActual = textReaderObj.ReadToEnd ();
Console.WriteLine (lineaActual);
}
}
catch (Exception e)
{
// Lógica de implementación para el manejo de la excepción generada
}

using permite, además, omitir el uso de la sentencia finally [14]. Este enfoque es mucho más intuitivo y expresivo comparado con el primer caso.

Hasta aquí hemos visto dos enfoques para asignación y liberación de recursos no gestionados usando las construcciones clásicas del lenguaje. Ahora es el turno de comprender un mecanismo más sofisticado: el patrón desechable, el cual es posible implementar en nuestros proyectos a través de la interfaz IDisposable [7].

La interfaz IDisposable define un método para la liberación de recursos, con el que el programador podrá crear su propia implementación lógica para liberar los recursos no manejados. Entendamos esto con un ejemplo práctico (adaptación de [15]):

A través de Dispose (bool marca) especificamos la lógica necesaria para terminar con los recursos adyacentes utilizados por la clase ManejadorAperturaDocumentos.

Podemos utilizar este recurso con la sentencia using:

using (ManejadorAperturaDocumentos mAD = new ManejadorAperturaDocumentos())
{
// sentencias asociadas con el uso de la funcionalidad de la clase ManejadorAperturaDocumentos
}

4. Conclusiones

Hemos aprendido acerca de los conceptos fundamentales de liberación de recursos y finalizadores en C#. Conceptos útiles cuando nos enfrentamos a proyectos que midan su calidad por el uso eficiente de los recursos disponibles por el ambiente de ejecución y la infraestructura adyacente. En particular, conocimos los básicos de la clase System.GC, que nos permite controlar la liberación de recursos (espacio en memoria); lo vimos a través de un ejemplo sencillo en el que se creaban 1000 instancias). Al final comprendimos los beneficios de implementar la interfaz IDisposable.

5. Glosario

  • CLR
  • Finalizadores
  • Gestión Automática de Memoria
  • Recolector de basura
  • Recurso gestionado
  • Recurso no-gestionado
  • Sistema operativo

6. Enlaces & Literatura

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: Finalize Methods and Destructors - http://msdn.microsoft.com/en-us/library/0s71x931.aspx
[3]: Object.Finalize Method (System) - http://msdn.microsoft.com/en-us/library/system.object.finalize.aspx
[4]: GC.SuppressFinalize Method (System) - http://msdn.microsoft.com/en-us/library/system.gc.suppressfinalize(v=vs.110).aspx
[5]: GC Class (System) - http://msdn.microsoft.com/en-us/library/system.gc.aspx
[6]: IDisposable.Dispose Method (System) - http://msdn.microsoft.com/en-us/library/system.idisposable.dispose.aspx
[7]: IDisposable Interface (System) - http://msdn.microsoft.com/en-us/library/System.IDisposable(v=vs.110).aspx
[8]: .net - Close and Dispose - which to call? - Stack Overflow - http://stackoverflow.com/questions/61092/close-and-dispose-which-to-call
[9]: Gestión de Memoria en C# | Experiencias Construcción Software - http://ortizol.blogspot.com/2013/06/gestion-de-memoria-en-c.html
[10]: GC.Collect Method (System) - http://msdn.microsoft.com/en-us/library/xe0c2357.aspx
[11]: Version Class (System) - http://msdn.microsoft.com/en-us/library/system.version.aspx
[12]: GC.GetTotalMemory Method (System) - http://msdn.microsoft.com/en-us/library/system.gc.gettotalmemory(v=vs.110).aspx
[13]: using Statement (C# Reference) - http://msdn.microsoft.com/en-us/library/yh598w02.aspx
[14]: try-finally (C# Reference) - http://msdn.microsoft.com/en-us/library/vstudio/zwc8s4fz.aspx
[15]: Como implementar correctamente IDisposable… | Can you hear the bits? - http://canyouhearthebits.wordpress.com/2008/08/08/como-implementar-correctamente-idisposable/


O

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.