Reference couting (conteo referencial).

Introducción.

¿Qué es el reference counting?

Reference counting es una técnica que se utiliza cuando necesitas mantener varias referencias a un mismo sitio, o más concretamente a un mismo objeto sin tener que tener varias copias de dicho objeto en memoria.

El problema derivado de hacer referencia a un objeto desde varios sitios consiste en la liberación del objeto, es decir, en el momento de liberar la memoria del objeto. Dicha liberación no constituye un problema en lenguajes con un buen recolector de basura (como C#) pero puede suponer una enorme fuente de memory leaks en otros proyectos. Por ejemplo, supongamos que tenemos algo como esto (el código es en c++ pero vale igual en delphi)

class CRecibo
{
    public:
      CCliente *cliente;    // Puntero a un objeto con algunos datos del objeto
       // Aqui vienen algunos metodos ...
}
class CFactura
{
    public:
      CCliente *cliente;
      // Otros metodos
}

y en algún momento del código tenemos algo como

   //..........
   factura = new CFactura();
   //... Rellenamos datos de la factura...
   recibo = new CRecibo();

   factura->cliente = MiCliente;
   recibo->cliente = MiCliente;  

Es un ejemplo muy traido por los pelos pero imaginemos que tenemos dos clases, la clase factura y la clase recibo y que ambas pertenecen a un determinado cliente. El cliente al que pertenecen se guarda en un puntero en cada clase. El problema surge a la hora de liberar la memoria, ¿cuando debemos liberar la memoria de la instancia del cliente?. Si la liberamos en el destructor de la clase CFactura podemos dejar instancias de CRecibo apuntando a direcciones de memoria invalida y si lo hacemos en el destructor de CRecibo ocurre lo contrario. Si lo hacemos en ambos, cuando intentemos hacer el free de la memoria nos dará una excepción el aquel destructor que haya entrado en último lugar (puesto que dicha memoria ya ha sido liberada).

Implementando un mecanismo de conteo de referencias.

Pese a que me gustaría decir: "hay una forma de resolver esto sin problemas y de una forma transparente" lo cierto es que no la hay o por lo menos yo no la conozco (aunque si la descubro prometo actualizar esto). Lo ideal es lo que realiza Delphi de forma automática con los interfaces.

Cuando creas un interface en Delphi (y de hecho en todos los lenguajes) debes heredar siempre del interfaz báscio que es el IUnknown y que define tres metodos: QueryInterface, AddRef y Release. No voy a entrar en detalles sobre la metódica de cada uno de ellos pero lo importante es que Delphi llama automáticamente a AddRef y a Release para los interfaces de forma que hay un solo objeto en memoria aunque haya varias referencias al objeto.

procedure Ejemplo(ObjQueSoportaUnInterfaceBasico : TMiObj);
var MiInterface : IMiInterfaceBasico;
begin
   // Al hacer el cast del objeto al interface Delphi automáticamente llama
   // a la funcion AddRef que aumenta el numero de referencias al interface
   MiInterface = ObjQueSoportaUnInterfaceBasico As IMiInterfaceBasico;
end;  // Al llegar aqui, Delphi automáticamente destruye la variable MiInterface
      // por que sale de contexto así que llama automáticamente al meotodo Release
      // que decrementa el numero de referencias... si el numero de referencias
      // es cero entonces y solo entonces libera el objeto.

Por desgracía no he encontrado la forma de conseguir un efecto como ese de forma automática, es decir, que el compilador automáticamente incremente el numero de referencias cuando sea necesario y lo decremente de la misma forma.

Para simular dicho comportamiento podemos intentar dar un pequeño rodeo. En realidad, lo que a mi me interesa es tener, de alguna manera, una copia del objeto compartido, pero también que dicho objeto sea compartido. Para ello lo más sencillo es que el propio objeto lleve la cuenta de las copias que hay repartidas de él por el programa, cuando no quede ninguna copia de dicho objeto, entonces podremos liberarlo. De está forma cada clase que tenga una copia estará encargada de liberarlo por que el objeto es suyo pero tansolo existirá una copia en memoria y además puesto que todas las clases liberan sus objetos evitaremos el problema de decidir quien debe liberar la memoria.

type MiObjeto = class
   private
      // El numero de referencias al objeto
      m_refCount : integer;
      // Algunos otros metodos
   public
      constructor Create;
      procedure Free;

      function Copy : MiObjeto;
end;

{ MiObjeto }
constructor MiObjeto.Create;
begin
   m_refCount := 1;  // En el constructor el
                     // numero de referencias es uno
end;

function MiObjeto.Copy : MiObjeto;
begin
  // Incrementar el numero de referencias
  // mediante la función de la API de windows
  // segura para multihilo
  InterlockedIncrement(m_refCount);
end;

procedure Free;
begin
  // Destruir si el numero de referencias
  // es cero.
  InterlockedDecrement(m_refCount);
  if m_refCount = 0 then
    Destroy;
end;

Una vez hecho esto en cualquiera cualquier sitio podemos hacer algo como

procedure TEjemplo.MiMetodo(factura : TFactura);
begin
  // Obtener una copia del objeto
  m_miReferencia := factura.cliente.Copy;
end;

destructor Destroy;
begin
  // Destruimos el objeto si está asignado
  // puesto que es "nuestro"
  if Assigned(m_miReferencia)
     m_miReferencia.Free;
end;

De esa forma nosotros somos dueños de nuestra referencia al objeto y debemos liberarla, y podemos hacerlo manteniendo la seguridad de que no estaremos destruyendo memoria que puede ser

10
Average: 10 (1 vote)
Your rating: None