Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interfaces, Anonymous Methods and Memory Leaks

this is a constructed example. I don't want to post the original code here. I tried to extract the relevant parts though.

I have an interface that manages a list of listeners.

TListenerProc = reference to procedure (SomeInt : ISomeInterface);

ISomeInterface = interface
   procedure AddListener (Proc : TListenerProc);   
end;

Now I register a listener:

SomeObj.AddListener (MyListener);

procedure MyListener (SomeInt : ISomeInterface);
begin
  ExecuteSynchronized (procedure
                       begin
                       DoSomething (SomeInt);
                       end);
end;

I do get memory leaks. Both the anonymous method and the interfaces are never freed. I suspect that this is due to some kind of circular reference here. The anonymous method keeps the interface alife and the interface keeps the anonymous method alife.

Two questions:

  1. Do you support that explanation? Or am I missing something else here?
  2. Is there anything I can do about it?

Thanks in advance!


EDIT: It's not so easy to reproduce this in an application small enough to post it here. The best I can do by now is the following. The anonymous method does not get released here:

program TestMemLeak;

{$APPTYPE CONSOLE}

uses
  Generics.Collections, SysUtils;

type
  ISomeInterface = interface;
  TListenerProc  = reference to procedure (SomeInt : ISomeInterface);

  ISomeInterface = interface
  ['{DB5A336B-3F79-4059-8933-27699203D1B6}']
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  end;

  TSomeInterface = class (TInterfacedObject, ISomeInterface)
  strict private
    FListeners          : TList <TListenerProc>;
  protected
    procedure AddListener (Proc : TListenerProc);
    procedure NotifyListeners;
    procedure Test;
  public
    constructor Create;
    destructor  Destroy; override;
  end;


procedure TSomeInterface.AddListener(Proc: TListenerProc);
begin
FListeners.Add (Proc);
end;

constructor TSomeInterface.Create;
begin
FListeners := TList <TListenerProc>.Create;
end;

destructor TSomeInterface.Destroy;
begin
FreeAndNil (FListeners);
  inherited;
end;

procedure TSomeInterface.NotifyListeners;

var
  Listener : TListenerProc;

begin
for Listener in FListeners do
  Listener (Self);
end;

procedure TSomeInterface.Test;
begin
// do nothing
end;

procedure Execute (Proc : TProc);

begin
Proc;
end;

procedure MyListener (SomeInt : ISomeInterface);
begin
Execute (procedure
         begin
         SomeInt.Test;
         end);
end;

var
  Obj     : ISomeInterface;

begin
  try
    ReportMemoryLeaksOnShutdown := True;
    Obj := TSomeInterface.Create;
    Obj.AddListener (MyListener);
    Obj.NotifyListeners;
    Obj := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
like image 891
jpfollenius Avatar asked Feb 16 '10 13:02

jpfollenius


People also ask

Can a memory leak happen in managed languages?

Common Types of Memory Leaks. Leaks in managed platforms are effectively references to an element that is no longer necessary. There are many samples of this, but they all boil down to discarding said reference. The most common problem is caching.

Can memory leaks occur in C#?

If you have many of those your application takes a lot of memory and eventually you run out of it. In C#, these are some common memory leaks: Not removing event listeners. Any event listener that is created with an anonymous method or lambda expression that references an outside object will keep those objects alive.


2 Answers

Your code is far from minimal. The following:

program AnonymousMemLeak;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TListenerProc  = reference to procedure (SomeInt : IInterface);

procedure MyListener (SomeInt : IInterface);
begin
end;

var
  Listener: TListenerProc;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    Listener := MyListener;
    Listener := nil;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

has the very same problem (Delphi 2009 here). This can't be worked or designed around. Looks to me like a bug in the compiler.

Edit:

Or maybe this is a problem of the memory leak detection. It has nothing to do with the parameter being an interface, a parameterless procedure leads to the same "leak". Very strange.

like image 78
mghie Avatar answered Oct 21 '22 10:10

mghie


Looks to me like a definite circular reference issue. Anonymous methods are managed through hidden interfaces, and if the TList<TListenerProc> is owned by the object that ISomeInterface is implemented on, then you've got a circular reference issue.

One possible solution would be to put a ClearListeners method on ISomeInterface which calls .Clear on the TList<TListenerProc>. As long as nothing else is holding a reference to the anonymous methods, that would make them all vanish and drop their references to the ISomeInterface.

I've done a few articles about the structure and implementation of anonymous methods that might help you understand what you're really working with and how they operate a little bit better. You can find them at http://tech.turbu-rpg.com/category/delphi/anonymous-methods.

like image 32
Mason Wheeler Avatar answered Oct 21 '22 10:10

Mason Wheeler