Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Delphi hängende Interfaces (https://www.delphipraxis.net/188088-haengende-interfaces.html)

stahli 29. Jan 2016 14:33

hängende Interfaces
 
Ich kann das Problem erst mal nur allgemein beschreiben.
Vielleicht kann ja dennoch jemand etwas dazu sagen...

Ich habe eine Factory, die mir diverse Objekte erzeugt und als Interfaces heraus gibt.
Jede Klasse unterstützt idR mehrere Interfaces und die Unterstützung prüfe ich mit Supports().

Jetzt benötige ich aber diverse gegenseitige Referenzierungen und Eintragungen in diversen Listen.

EurekaLog zeigt mir entsprechend auch MemoryLeaks an, da die Objekte NATÜRLICH nicht aufgelöst werden.

ABER wiederum zeigt mit EurekaLog nur einzelne Objekte an, nicht aber die 1000 erzeugten Objekte von TMyClass, obwohl deren Destructoren aber definitiv nie durchlaufen wurden.


Ich habe daher drei grundsätzliche Fragen:

- Gibt es so von der Ferne eine Erklärung, warum EurekaLog meine 1000 Objekte nicht bemeckert, obwohl die definitiv nicht aufgelöst wurden?

- Gibt es eine Möglichkeit die "hängenden Referenzen" herauszufinden? Mir würde nur einfallen, das Projekt zu entkernen und Stück für Stück neu zusammen zu setzen und zu profilen.

- Inzwischen denke ich, dass die automatische Referenzzählung für mich doch nicht so sinnvoll ist. Interfaces möchte ich zwar wegen der austauschbaren Funktionalitäten und einheitlicher öffentlicher Schnittstellen weiter nutzen, aber die Lebenszeit und gegenseitige Referenzen würde ich doch lieber wieder selbst verwalten.

Den Themenkomplex hatten wir schon mal behandelt.
http://www.delphipraxis.net/166899-i...eferenzen.html
http://www.delphipraxis.net/159095-r...objekte-2.html

Ich werde jetzt mal in Richtung von Bummis Vorschlag überlegen: http://www.delphipraxis.net/1134394-post46.html.
Dann erzeugt die Factory auf Anforderung bestimmte Objekte und verwaltet auch deren Lebenszeit und die Freigabe.
Referenzen werden dann über eine ID zugewiesen. Wenn benötigt wird ein entsprechendes Interface anhand der ID abgerufen und nach Verwendung beim rauslaufen aus dem Scope wieder Counter-Reduziert.
Wenn das Abrufen über ein Dictionary oder binäre Liste läuft wäre das ja auch performant.

Dann gäbe es halt nicht so etwas wie
Delphi-Quellcode:
TPerson.NameDesVaters: string
begin
  Result := Vater.Name;
end;
sondern
Delphi-Quellcode:
TPerson.NameDesVaters: string
var
  Vater: IPerson;
begin
  // TPerson kennt nur eine "VaterId"
  Vater := TFactory.GetPerson(VaterId); // Wird aus dem Dict geholt bzw. ggf. zuvor wenn nötig instanziiert
  Result := Vater.Name;
end;
// Referenz auf Vater ist wieder aufgelöst

Die Interface-Objekte werden freigegeben, wenn die Factory diese aus ihrer Sammlung entfernt.

Wenn ich das so umsetzen kann sind die ersten beiden Fragen zwar im Grunde hinfällig, aber dennoch würden mich Antworten interessieren.

mjustin 29. Jan 2016 15:23

AW: hängende Interfaces
 
Bei gegenseitigen Referenzierungen verwende ich DUnit Tests, in denen Varianten der Erzeugung und Freigabe ausgeführt werden. Mit ReportMemoryLeakDetailsOnShutDown kontrolliert der DUnit TestRunner beim Beenden, ob es noch nicht freigegebene Instanzen gibt.

Da ich bisher alle Referenzprobleme lösen konnte (die Bibliotheken laufen auch unter Free Pascal mit TInterfacedObject ohne Memory Leaks), verwende ich die Referenzzählung mittlerweile ohne Bauchschmerzen. In hartnäckigen Fällen hilft es, die Anzahl der Referenzen auszugeben, die vor und nach Operationen mit einerm Interface existieren.

Stevie 29. Jan 2016 15:33

AW: hängende Interfaces
 
DUnit Tests mit LeakCheck und PODOs nich als Interfaces nutzen.

jaenicke 29. Jan 2016 22:52

AW: hängende Interfaces
 
Zitat:

Zitat von stahli (Beitrag 1328748)
Jetzt benötige ich aber diverse gegenseitige Referenzierungen und Eintragungen in diversen Listen.

EurekaLog zeigt mir entsprechend auch MemoryLeaks an, da die Objekte NATÜRLICH nicht aufgelöst werden.

So natürlich ist das nicht. Wir haben auch Interfaces, in denen andere Interfaces sich in einem Listener registriert haben um bei Ereignissen benachrichtigt zu werden, die aber das andere Interface kennen.
Aber da werden dann auch die Listen mit den Listenern beim Shutdown geleert und dann erst alles andere abgebaut, das gehört zum regulären Shutdown-Prozess der Anwendung.

EgonHugeist 30. Jan 2016 05:55

AW: hängende Interfaces
 
@Stahli

Wie man das im einzelnen Tracken kann, weiß ich nicht, ich passe auf );

jedoch du kannst auch "weak" Referencen nutzen. eg. TContainedObject oder dir die Referencen selber abbilden.
Code:
private
 fWeakMyInterface: Pointer;
 fMyInterface: IMyInterface;

...

var
  MyInterface: IMyInterface;
begin
  MyInterface := TMyInterfacedObject.Create(Self);
  fWeakMyInterface:= Pointer(MyInterface);
end;
Dann jedoch mußt du aber auch Nachdem Erschaffen des Interfaces dir die Referenz merken und 'nen DeRegister code mit reinfummeln, wenn ein Interface aus dem Speicher geworfen wird.

Somit kann man zirkuläre Interface Referenzen auflösen, jedoch können sich beide immernoch gegenseitig referenzieren: IMyInterface(fWeakMyInterface).DoSomething;
Der Owner sollte die schwache Reference haben, das SubInterface sollte den Owner direkt als Interface referenziern, damit dein DeRegister nicht auf ein gelöschtes interface knallt.

Dann kannst du beim aufräumen des Owners mit zB. procedure TMyInterfacedObject.Close; das Schwach-Referenzierte Interface, falls <> nil, zuerst zu einem Close zwingen. Es Deregistriert sich zuerst beim Owner(fWeakMyInterface wird genillt) und dann löscht es die Interface Referenz zum Owner, und schon lößen sich alle voneinander, beide wissen, es gibt keine weiteren Referenzen mehr, und der Speicher wird frei gegeben.

stahli 1. Mär 2017 22:38

AW: hängende Interfaces
 
Liste der Anhänge anzeigen (Anzahl: 1)
Ich will das Thema nochmal aufgreifen...

Weak Referenzen kann ich mit XE3 nicht nutzen. Das Arbeiten mit Pointern finde ich auch nicht optimal.

DUnit und LeakCheck kann ich nicht wirklich nachvollziehen. :oops:


Ich habe daher mal ein kleines Projekt gebastelt, um einen Ansatz zu suchen.
Dafür habe ich ein Basisinterface ICleanUp und eine Basisklasse TCleanUp erstellt, die mögliche gegenseitige Referenzen abbauen können.

Man muss allerdings für alle betroffenen Interfaces/Objekte CleanUp(MyIntf) aufrufen.

Ich hänge das Projekt an (XE3, nur Quelle, ohne EurekaLog-Aktivierung).
Hier auch ein Video dazu: https://youtu.be/2yjGr2CoT5E

Mich würde mal Eure Meinung dazu interessieren sowie natürlich auch bessere Lösungen.

In dem Zusammenhang hatte ich überlegt, dass man (bzw. Emba) doch evtl. loggen könnte, welche Referenzen erzeugt und nicht wieder freigegeben werden. Man müsste dazu in _AddRef und _Release m.E. die Speicherstelle der Variable, die Speicherstelle des Objektes und die Quelltextstelle in einer Liste sammeln bzw. wieder löschen.
Einträge, die zum Schluss übrig bleiben, wurden nicht freigegeben.

Ich versuche mal, das zu skizzieren (A und B sind Interfaces, die sich gegenseitig referenzieren können, die Nr am Anfang ist eine Programmzeile):

begin
...
10: A1 := TA.Create
-> Eintrag: Adr(A1) $12345 / Adr(ObjektA1) $456789 / Zeile 10
11: B1 := TB.Create
-> Eintrag: Adr(B1) $12346 / Adr(ObjektB1) $45678A / Zeile 11
...
18: A1.B := B1
-> Eintrag: Adr(A1.fB) $23456 / Adr(ObjektB1) $45678A / Zeile 18
19: B1.A := A1
-> Eintrag: Adr(B1.fA) $23457 / Adr(ObjektA1) $456789 / Zeile 19
...
end
-> löschen: Adr(A1) $12345 / Adr(ObjektA1) $456789 / Zeile 10
-> löschen: Adr(B1) $12346 / Adr(ObjektB1) $45678A / Zeile 11

Also direkt in _AddRef und _Release müsste sowohl die Speicherstelle des zu referenzierenden Objektes bekannt sein als auch die Speicherstelle der Variablen. Dann könnten dort die skizzierten Einträge erzeugt werden und bei Vorliegen von Debug-Infos auch Bezüge zum Quelltext.

Im Beispiel werden beim Prozeduraussprung die Referenzen auf ObjektA1 und ObjektB1 ja je einmal verringert.
Der Debugger weiß ja auch, dass die Variablen A1 und B1 aus dem Scope fallen. Also könnte er erkennen, dass die ersten beiden Einträge obsolet sind und könnte diese löschen oder kennzeichnen.

Somit würden zum Programmende folgende zwei Einträge übrig bleiben:
Adr(A1.fB) $23456 / Adr(ObjektB1) $45678A / Zeile 18
Adr(B1.fA) $23457 / Adr(ObjektA1) $456789 / Zeile 19

Damit könnte man die nicht aufgelösten Referenzen sehr zuverlässig und komfortabel finden.
Emba müsste dazu entsprechende Logs in _AddRef und _Release ermöglichen.

Ist das so denkbar oder ist das in der Form unmöglich?



Ein Reference Tracing bietet übrigens AQTime Pro inzwischen an.
https://www.youtube.com/watch?v=y4qbNwuk2Qs
Man muss jedoch EurekaLog dazu ggf. im Projekt deaktivieren.
Also kann man mit EurekaLog hängende Referenzen finden, dieses dann ausschalten und die Ursache mit AQTime näher untersuchen.
Ich muss allerdings sagen, dass ich das auch nicht so sehr übersichtlich finde.

Zacherl 2. Mär 2017 01:47

AW: hängende Interfaces
 
Zitat:

Zitat von stahli (Beitrag 1362988)
Also direkt in _AddRef und _Release müsste sowohl die Speicherstelle des zu referenzierenden Objektes bekannt sein als auch die Speicherstelle der Variablen.

Die Speicherstelle des Objektes kannst du über einen Hook von
Delphi-Quellcode:
TInterfacedObject._AddRef
bzw.
Delphi-Quellcode:
TInterfaced._Release
auf jeden Fall loggen (
Delphi-Quellcode:
Self
). Die Speicherstelle der Variable wirst du aber nicht ermitteln können.
Dass Embacadero da irgendwas in die Richtung einbaut halte ich auch für recht unwahrscheinlich. Der automatische Aufruf von
Delphi-Quellcode:
_AddRef
und
Delphi-Quellcode:
_Release
ist sowieso schon ein "Hack".

Zitat:

Zitat von stahli (Beitrag 1328748)
Inzwischen denke ich, dass die automatische Referenzzählung für mich doch nicht so sinnvoll ist. Interfaces möchte ich zwar wegen der austauschbaren Funktionalitäten und einheitlicher öffentlicher Schnittstellen weiter nutzen, aber die Lebenszeit und gegenseitige Referenzen würde ich doch lieber wieder selbst verwalten.

Also falls du die Schnittstellen nur in deinem Delphi Programm verwendest und nicht z.b. aus einer C++ DLL heraus damit hantieren willst, dann könntest du die Interfaces auch durch abstrakte Klassen ersetzen.

Stevie 2. Mär 2017 02:24

AW: hängende Interfaces
 
Imo leider eine einfache Sache total overengineered.

Dein Cleanup und CleanupRef is unnötig, einfach sowas wie IDisposable implementieren und
im Dispose die eigenen Interfaces sofern auch IDisposable Dispose aufrufen
und dann auf nil setzen und schon ist jegliche zirkuäre Referenz aufgelöst.

Im übrigen möchte ich mal behaupten, wenn man so massive zirkuläre Interface Referenzen hat,
dann stimmt was mit der Architektur nicht oder man überstrapaziert sie, wo sie nicht hingehören.

stahli 3. Mär 2017 12:23

AW: hängende Interfaces
 
Liste der Anhänge anzeigen (Anzahl: 1)
Zitat:

Zitat von Zacherl (Beitrag 1362990)
Die Speicherstelle der Variable wirst du aber nicht ermitteln können.
Dass Embacadero da irgendwas in die Richtung einbaut halte ich auch für recht unwahrscheinlich. Der automatische Aufruf von
Delphi-Quellcode:
_AddRef
und
Delphi-Quellcode:
_Release
ist sowieso schon ein "Hack".

Dass ich das nicht ermitteln kann dachte ich mir. Aber der Compiler weiß ja eigentlich, wohin er die Referenz schreibt (also wo der Speicherplatz der Variablen liegt). Das würde ich jedenfalls voraussetzen. Daher dachte ich, dass Emba einen entsprechenden Log erzeugen könnte.
Aber so genau kann ich das natürlich nicht wissen.


Zitat:

Zitat von Zacherl (Beitrag 1362990)
Also falls du die Schnittstellen nur in deinem Delphi Programm verwendest und nicht z.b. aus einer C++ DLL heraus damit hantieren willst, dann könntest du die Interfaces auch durch abstrakte Klassen ersetzen.

Mit Interfaces will ich auf jeden Fall arbeiten und i.d.R. auch die Referenzzählung nutzen. Für die Sonderfälle der gegenseitigen Referenzierungen muss man dann halt eine explizite Lösung finden.



@Stevie

Du hast Recht.

Ich habe mein ICleanUp nochmal etwas umgebaut. Es sollte jetzt eher dem entsprechen, was Du mit IDisposable gemeint hast.
Ich rufe jetzt einfach Kill(MyIntf) auf (analog FreeAndNil(MyObj)).
Die Funktion CleanUpRef habe ich mal noch drin gelassen, aber sie ist i.d.R. nicht erforderlich.

Die häufigen gegenseitigen Referenzen waren Singleton-InterfaceObjekte, die sich alle gegenseitig benötigen und die ich den anderen Objekten jeweils gegenseitig bekanntgegeben hatte (entweder im constructor oder durch spätere Registrierung.

Jetzt habe ich diese doch wieder global bereitgestellt, was die Referenzierungsorgie natürlich verringert.

Stevie 3. Mär 2017 15:50

AW: hängende Interfaces
 
Ich würd das ja so implementieren:

Delphi-Quellcode:
unit DisposableObject;

interface

type
  IDisposable = interface
    ['{07751839-9A68-47C0-9116-68D0D5D7956F}']
    procedure Dispose;
  end;

  TDisposableObject = class(TObject, IInterface, IDisposable)
{$IFNDEF AUTOREFCOUNT}
  private const
    objDestroyingFlag = Integer($80000000);
    function GetRefCount: Integer; inline;
{$ENDIF}
  protected
{$IFNDEF AUTOREFCOUNT}
    [Volatile] FRefCount: Integer;
    class procedure __MarkDestroying(const Obj); static; inline;
{$ENDIF}
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    procedure Dispose; virtual;
  public
{$IFNDEF AUTOREFCOUNT}
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    class function NewInstance: TObject; override;
    property RefCount: Integer read GetRefCount;
{$ENDIF}
  end;

procedure DisposeAndNil(var intf);

implementation

procedure DisposeAndNil(var intf);
var
  disposable: Pointer;
begin
  if Assigned(IInterface(intf)) and (IInterface(intf).QueryInterface(IDisposable, disposable) = 0) then
  begin
    IInterface(intf) := nil;
    IDisposable(disposable).Dispose;
    IDisposable(disposable) := nil;
  end;
end;

{ TDisposableObject }

{$IFNDEF AUTOREFCOUNT}

function TDisposableObject.GetRefCount: Integer;
begin
  Result := FRefCount and not objDestroyingFlag;
end;

class procedure TDisposableObject.__MarkDestroying(const Obj);
var
  LRef: Integer;
begin
  repeat
    LRef := TDisposableObject(Obj).FRefCount;
  until AtomicCmpExchange(TDisposableObject(Obj).FRefCount, LRef or objDestroyingFlag, LRef) = LRef;
end;

procedure TDisposableObject.AfterConstruction;
begin
  AtomicDecrement(FRefCount);
end;

procedure TDisposableObject.BeforeDestruction;
begin
  if RefCount <> 0 then
    Error(reInvalidPtr);
end;

procedure TDisposableObject.Dispose;
begin
end;

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

{$ENDIF AUTOREFCOUNT}

function TDisposableObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TDisposableObject._AddRef: Integer;
begin
{$IFNDEF AUTOREFCOUNT}
  Result := AtomicIncrement(FRefCount);
{$ELSE}
  Result := __ObjAddRef;
{$ENDIF}
end;

function TDisposableObject._Release: Integer;
begin
{$IFNDEF AUTOREFCOUNT}
  Result := AtomicDecrement(FRefCount);
  if Result = 0 then
  begin
    Dispose;
    __MarkDestroying(Self);
    Destroy;
  end;
{$ELSE}
  Result := __ObjRelease;
{$ENDIF}
end;


end.
Der Sinn und die Implementierung von DisposeAndNil liegt darin, dass damit nil Sicherheit gewährleistet und Rekursivität verhindert wird.

Das ganze IFDEF Zeugs liegt daran, dass ich einfach stumpf TInterfacedObject aus System kopiert habe, weil ich nicht davon ableiten wollte/konnte,
da ich eine zusätzliche Zeile in _Release hinzugefügt habe (Dispose aufrufen vorm Destroy). In das Destroy wollt ich es nicht hinzufügen,
weil man dann möglicherweise rekursive Dispose Aufrufe während eines Destroy Vorgangs hätte und dann diese auch noch gegen eventuelle Aktionen im Destroy
absichern hätte müssen (z.B. Überprüfung auf nil von fList in TD).

Das führt zu implementierungen von Dispose in z.B. TA wie folgt:

Delphi-Quellcode:
procedure TA.Dispose;
begin
  inherited;
  DisposeAndNil(fB);
  DisposeAndNil(fC);
end;
Wichtig: Das ganze hat trotz Namensähnlichkeit nix mit TObject.DisposeOf zu tun. Das ist nämlich a) nicht überschreibbar b) macht auf nicht ARC nix anderes als Free aufzurufen und am wichtigsten c) ruft auf ARC nur den Destructor auf aber gibt den Speicher nicht frei. Die zurückgelassene Instanz ist dann ein Zombie, denn es wird nicht CleanupInstance aufgerufen so dass gemanagte nicht explizit auf leer/nil gesetzte Felder noch gesetzt sind (siehe RSP-14682).


Alle Zeitangaben in WEZ +1. Es ist jetzt 06:09 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz