Published by Joao Morais on 06 Sep 2008

Objetos com contagem de referência

Imagine um data module, que por motivo de economia de memória é criado e destruído com a aplicação em execução. Imagine este data module sendo referenciado por mais de um formulário ao mesmo tempo. Como garantir que o data module será destruído apenas quando nenhum formulário estiver apontando para ele?

Este é o objetivo da contagem de referência. Um membro da classe guarda a quantidade de objetos que apontam para ele. Logo que esta contagem cai para zero, significa que o objeto pode ser destruído. A estrutura da classe base fica assim:

TRefCountObject = class(TObject)
private
  FRefCount: Integer;
public
  function AddRef;
  class function NewInstance: TObject; override;
  function Release;
end;

e as respectivas implementações:

function TRefCountObject.AddRef;
begin
  Result := InterlockedIncrement(FRefCount);
end;

class function TRefCountObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TRefCountObject(Result).FRefCount := 1;
end;

function TRefCountObject.Release;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result = 0 then
    Free;
end;

Como isto funciona? Quando o programador cria uma instância, o FRefCount é setado para 1. Não importa como ele chama o construtor, ou se ele sobrecarrega algum método e não chama inherited, o FRefCount sempre começa em 1. Cada nova referência, indicada através da chamada ao método AddRef, aumenta o contador de referências do objeto. Cada um que liberar aquele objeto chama o método Release. O último que liberar o objeto, fará com que ele se auto destrua.

Na implementação acima, as rotinas Interlocked trabalham de forma segura em ambiente multi thread, ao contrário do inc e dec tradicionais. Elas estão declaradas na unit Windows no Delphi, e na unit System no FPC. O método NewInstance intercepta o momento exato em que a instância é criada, e neste momento seta o valor do contador de referência.

Agora a primeira limitação desta implementação: programadores habituados com o controle do tempo de vida do objeto no Object Pascal tradicional vão precisar alterar todas as suas chamadas a .Destroy e a .Free para que a rotina funcione corretamente. E mesmo depois de alterar, precisarão habituar-se com esta nova filosofia, o que pode gerar códigos mais sujeitos a erros. O culpado é o destructor que, independente de ser virtual, uma vez chamado fará com que a instância seja destruída.

Por sorte, assim como existe um método virtual que é responsável por criar a instância, existe também outro que é responsável por destrui-la. Sobrecarregar este método e tratá-lo da mesma forma que o release, fará com que o .Free não destrua o objeto antes da hora. Veja a nova declaração da classe:

TRefCountObject = class(TObject)
private
  FRefCount: Integer;
  function Release;
public
  function AddRef;
  function FreeInstance; override;
  class function NewInstance: TObject; override;
end;

e a implementação. Note que o Release tem que ser reescrito para entrar no jogo do novo FreeInstance:

function TRefCountObject.Release;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result < 0 then
    raise Exception.Create('Não é possível liberar a instância');
end;

function TRefCountObject.FreeInstance;
begin
  if Release = 0 then
    inherited;
end;

Os demais métodos continuam idênticos, e o Release passa a ser um coadjuvante. Ele até poderia ser usado para liberar contagem de referência, mas não é capaz de destruir uma instância a fim de evitar repetição de código. Por este motivo ele foi movido para a área private e passa a ser usado apenas internamente.

Este novo modelo está quase perfeito. Ao criar o objeto sua contagem é um. Ao chamar AddRef a contagem salta para dois. Ao chamar Destroy ou Free a contagem de referências cai para 1 e o inherited FreeInstance não é chamado, o que mantém o objeto na memória. Na segunda chamada ao Free, o objeto é liberado pois ninguém mais aponta para ele (FRefCount igual a zero).

A falha deste modelo é, ao meu ver, uma limitação do modelo de objeto do Object Pascal. O TObject foi desenhado pensando em um modelo aonde o tempo de vida de um objeto é determinado fora da instância. Ou seja, uma instância A que faz referência a uma instância B é a responsável por destruí-la, enquanto um modelo com um certo grau de coerência deveria deixar esta decisão para a própria instância B, e a instância A deveria limitar-se a avisar B que tem menos gente apontando para ela.

A parte do controle do tempo de vida do objeto já está resolvido, FreeInstance foi sobrecarregado e .Free vai meramente decrementar a contagem de referência quando esta for maior do que 1. No entanto o destructor padrão do TObject, o método Destroy, é virtual e é a opção lógica para ser sobrecarregado quando uma determinada ação é necessária durante a destruição do objeto. O problema do nosso novo modelo é que FreeInstance é chamado depois do Destroy, e qualquer rotina escrita em um Destroy sobrecarregado será chamado mesmo que a instância não seja destruída.

A forma de resolver este problema é ‘matar’ o destructor virtual e impedir que o programador pendure código nele, e por outro lado, criar outro método virtual que é chamado a fim de notificar a destruição real do objeto. A declaração da classe fica assim:

TRefCountObject = class(TObject)
private
  FRefCount: Integer;
  function Release;
protected
  procedure Finit; virtual;
public
  destructor Destroy; reintroduce;
  function AddRef;
  function FreeInstance; override;
  class function NewInstance: TObject; override;
end;

e o que muda na declaração da classe:

destructor TRefCountObject.Destroy;
begin
end;

function TRefCountObject.Finit;
begin
end;

function TRefCountObject.FreeInstance;
begin
  if Release = 0 then
    try
      Finit;
    finally
      inherited;
    end;
end;

Agora a classe está pronta para criar instâncias que entendam contagem de referência sem perder a tradicional interface Create/Destroy/Free do Object Pascal.

Segue abaixo a declaração completa da classe, após todas as alterações:

TRefCountObject = class(TObject)
private
  FRefCount: Integer;
  function Release;
protected
  procedure Finit; virtual;
public
  destructor Destroy; reintroduce;
  function AddRef;
  function FreeInstance; override;
  class function NewInstance: TObject; override;
end;

...

function TRefCountObject.AddRef;
begin
  Result := InterlockedIncrement(FRefCount);
end;

destructor TRefCountObject.Destroy;
begin
end;

function TRefCountObject.Finit;
begin
end;

function TRefCountObject.FreeInstance;
begin
  if Release = 0 then
    try
      Finit;
    finally
      inherited;
    end;
end;

class function TRefCountObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TRefCountObject(Result).FRefCount := 1;
end;

function TRefCountObject.Release;
begin
  Result := InterlockedDecrement(FRefCount);
  if Result < 0 then
    raise Exception.Create('Não é possível liberar a instância');
end;

Published by Joao Morais on 31 Aug 2008

Forms management tip

(portuguese)

From my point of view, one thing that is really missing for Delphi/Lazarus is a better application’s form management. Two problems: an independent public variable is used to control an instance, and the ‘please place everything within the form’ approach. The first one requires the programmer to either create the form at the start of the application and not destroy it anymore, or it requires the programmer to create the forms and keep track of object destruction insuring that no variable points to a destroyed object. The second, among other problems, requires that the first one works perfectly so that the communication between forms can work. Continue Reading »

Published by Joao Morais on 31 Aug 2008

Dica para gerenciamento de formulários

(english)

Ao meu ver, uma parte bem mal feita do Delphi/Lazarus é o gerenciamento de formulários da aplicação. Tanto o fato de usar uma variável pública independente para controlar a instância quanto a isca ‘jogue tudo no form’ trazem problemas. A primeira abordagem exige que o programador, ou crie os formulários no início da aplicação e não os destrua mais, ou exige que ele cuide para não deixar a variável apontar para um objeto destruído. A segunda abordagem, entre outros problemas, exige que a primeira funcione perfeitamente para que ocorra comunicação entre formulários. Continue Reading »

Published by Joao Morais on 23 Aug 2008

Ponteiros e alocação dinâmica

Tem um ditado que diz: Eu não sei, mas tenho o telefone de quem sabe. Trabalhar com ponteiros é muito parecido com isto, mas o ditado fica assim: Eu não sei, mas tenho o endereço de quem sabe. Continue Reading »

Published by Joao Morais on 16 Aug 2008

Decouple MVP and VCL with Interfaces.

(portuguese)

Contrary to some object oriented programming gurus, I was not much into interfaces. The first uses were under utilized, as just a memory manager assistant (interfaces in object pascal have reference counting maintained by the compiler, contrary to classes). Continue Reading »

Published by Joao Morais on 16 Aug 2008

Desacoplar MVP e VCL com Interfaces

(english)

Ao contrário de alguns astros da programação orientada a objetos, eu não era muito fã de interface. Os primeiros usos foram sub-utilizados, como mero auxiliar de gerenciamento de memória (interface em Object Pascal tem contagem de referência controlada pelo compilador, ao contrário de classes). Continue Reading »

Published by Joao Morais on 09 Aug 2008

do jmp às Interfaces - A história do acoplamento

No princípio era o jmp. E o call. E o ret. E os saltos condicionais. E com isto os heróicos programadores da época conseguiam criar laços e blocos cuja execução dependia de alguma condição ter sido satisfeita. E a máquina começou a pensar. Um label indicava ao assembler aonde determinada rotina estava começando, e o assembler transformava aquele label em um endereço de memória. Entre o label e o ret estava o que conhecemos hoje por procedure. Continue Reading »

Published by Joao Morais on 10 Feb 2008

Construindo um interpretador orientado a objetos

Outro dia precisei colocar fórmula em um dos sistemas que desenvolvo, para que o usuário tenha mais liberdade para informar como um custo deva ser calculado. É permitido que ele faça algo como 0.012 * 66 * 96 * Chapas / FormatoImpressao, aonde as variáveis apresentadas são atributos do objeto de negócio. Continue Reading »

Published by Joao Morais on 06 Feb 2008

Como remover vazamentos de memória

Ferramentas como FastMM melhoram o desempenho de aplicações que utilizam muita alocação dinâmica, e de quebra ainda relata se a aplicação está deixando vazamentos de memória.

Agora saber que a aplicação tem vazamento de memória é fácil, no entanto, dependendo do tamanho do projeto, torna-se mais difícil determinar aonde ela foi alocada. Continue Reading »

Published by Joao Morais on 30 Nov 2007

Wiki do PressObjects

Há alguns posts atrás prometi colocar alguns artigos sobre PressObjects neste blog. Depois de terminar o rascunho do primeiro artigo, notei que ele tem um foco diferente do que tem sido aplicado aqui, sem falar que o próprio projeto tem muito pouco material. Resolvi escrever tais artigos diretamente para o projeto, e para tanto dei início a um wiki que você pode conferir aqui.

Estes primeiros artigos vão para o wiki num instante, e vou deixar para o blog a missão de mandar algum recado ou publicar material que envolva ponto de vista.

Next »