DisposeOf, ARC, Free, WEAK a další

vložil Radek Červinka 11. července 2014 22:28

Laskavý čtenář mého blogu jistě už naznal, že Delphi neznamená jen Win32 (Win64, OSX) ale i kompilátory pro iOS a Android.

Tyto kompilátory (kromě kompilace do ARM) přinášejí i podporu ARC (Automatic Reference Counting) pro objekty (podobně jako k tomu došlo před nedávnem Apple). Princip není v Delphi v podstatě nový - Delphi odjakživa používají stejný mechanismus nejméně pro řetězce a interface.

V podstatě existují dvě možnosti automatického uvolňování paměti - ARC a Garbage Collector, kdy GC je ten nedeterministický (tj. problematický viz Android a nový runtime ART to taky řeší), ale to sem asi nepatří.

Obecně ale oba modely musí řešit cyklické reference. Takže ARC je moc pěkné, ale není vše tak sluníčkové jak by mohlo vypadat. V nejhorším ale jen nedojde k uvolnění, pokud se tomu chceme vyhnout, je nutno jít trochu hlouběji. Upozorňuji, že právě následující text jde v některých detailech hlouběji než je nutné pro normálního programátora.

Pojďme ale od začátku. Záleží na tom, zda budete psát nový kód nebo migrovat starší. V prvém případě si stačí uvědomit několik věcí a není co řešit. Horší situace je s úpravou staršího kódu, který je sice momentálně bezchybný, ale čert nikdy nespí a některé reference se nemusí uvolnit.

Takže v ARC to funguje následovně: Vytvoříte objekt a používáte ho. V případě, že zmizí z viditelnosti, jeho počítadlo referencí se sníží a pokud je 0 objekt je uvolněn. Nesmíte volat destructor přímo (píši destruktor! ne Free!), jelikož se tím dostáváte do interference s ARC (uvolníte objekt a v okamžiku kdy se dostanete mimo viditelnost by se ARC snažilo o uvolnění).

Klasický pattern pro Delphi objekty je

var
  obj: TMyObject;
…
  obj := TMyObject.Create;
  try
  ….
  finally
   obj.Free; // nebo FreeAndNil(obj);
  end; 

V ARC můžete klidně try..finally + Free vynechat a objekt bude uvolněn dle dříve uvedeného pravidla. Problém je, pokud sdílíte kód s ne-ARC Delphi kompilátorem. Delphi v případě ARC kompilátoru přeloží volání Free jako přiřazení nil a ve většině případů to bude stačit, jelikož se tím sníží počet referencí a dojde k uvolnění objektu. V ne-ARC Delphi to bude fungovat jako předtím.

Pro dokreslení kousek zdrojového kódu:


procedure TObject.Free;
begin
// under ARC, this method isn't actually called since the compiler translates
// the call to be a mere nil assignment to the instance variable, which then calls _InstClear
{$IFNDEF AUTOREFCOUNT}
  if Self <> nil then
    Destroy;
{$ENDIF}
end;

procedure TObject.DisposeOf;
type
  TDestructorProc = procedure (Instance: Pointer; OuterMost: ShortInt);
begin
{$IFDEF AUTOREFCOUNT}
  if Self <> nil then
  begin
    Self.__ObjAddRef; // Ensure the instance remains alive throughout the disposal process
    try
      if __SetDisposed(Self) then
      begin
        _BeforeDestruction(Self, 1);
        TDestructorProc(PPointer(PByte(PPointer(Self)^) + vmtDestroy)^)(Self, 0);
      end;
    finally
      Self.__ObjRelease; // This will deallocate the instance if the above process cleared all other references.
    end;
  end;
{$ELSE}
  Free;
{$ENDIF}
end;

Problémem je něco co se nazývá vazba rodič - dítě, resp. okamžik kdy dítě drží referenci na rodiče. V tomto případě by objekt žil navždy. Je nutno jednu z těchto referencí jako WEAK. Tyto reference nejsou počítány a dojde ke správnému uvolnění.

  TParent = class
  public
     Child: TChild;
  end;
 
  TChild = class
  public
    [Weak] Parent: TParent;
    constructor Create(aParent: TParent);
  end;
 
  constructor TChild.Create(aParent: TParent);
  begin
    inherited;
    Parent := aParent;
  end;
 
var
  p: TParent;
 
begin
  p := TParent.Create;
  p.Child := TChild.Create(p);
end; // <- p je mimo viditelnost (out of scope)
 

V uvedeném kódu v okamžiku kdy p zmizí s viditelnosti, destructor bude zavolán. V případě, že se atribut weak odstraní, tak ve třídě TChild.Parent by byla ještě držena reference (tj. reference count = 1) a objekt není uvolněn.

Takže jasné definování weak a normální reference je důležité.

Ale, jelikož zde vstupuje do hry historie Delphi, např. TComponent notification system. Prostě pro starší kód označení Weak nemusí stačit.

A proto byla přidána metoda DisposeOf, která vyvolá destrutor explicitně.

Příkladem je TForm.

var
  f: TMyForm;
begin
  f := TMyForm.Create(nil);
  f.ShowModal;
end; // 1. <- f je out of scope
 
nebo
 
begin
  f := TMyForm.Create(nil);
  try
    f.ShowModal;
  finally
    f.Free; // 2. <- call destructor
  end;
end;
 

Upozorňuji, že každý Form má referenci v globální proměnné Screen.Forms.

V ne=ARC Delphi první případ je instance držena objektem Screen.Forms během celé života aplikace, v druhém Free vyvolá zmíněný notifikační systém, který v důsledku odstraní referenci ze Screen.Forms. Vše OK.

V ARC Delphi v prvním případě nedojde k uvolnění objektu (referenci drží Screen.Forms), v druhém případě Free je nahrazeno nil (ale reference je držena Screen.Forms), tj. destructor není vyvolán.

Řešení: manuálně zavolat destructor via metody DisposeOf, tím se spustí řetězec notifikací a OK. Takže DisposeOf je takový háček, pro starý kód.

Pozn: DisposeOf je podobné IDisposable z C#, tam je stejný problém, jen je GC vyvoláván až je nejhůře, narozdíl od ARC, kdy je objekt uvolněn hned jak je to možné.

Myslím si (a nejsem sám), že se EMBT snaží postupně toto minimalizovat a nahrazovat konstrukcemi, které jsou pro ARC přirozenější.

Postup konverze starého kódu dle EMBT je:

  • identifikovat WEAK reference
  • v krizi použít DisposeOf (prakticky by to mělo být minimálně)

Nový kód je samozřejmě něco jiného.

Na závěr ještě alternativní řešení, které samo EMBT používá ve FireDACu (FireDAC.Stan.Util), asi tam těch kruhových referencí je hafo.


{-------------------------------------------------------------------------------}
procedure FDFree(AObj: TObject);
begin
  if AObj <> nil then
{$IFDEF AUTOREFCOUNT}
    AObj.DisposeOf;
{$ELSE}
    AObj.Destroy;
{$ENDIF}
end;

{-------------------------------------------------------------------------------}
procedure FDFreeAndNil(var AObj);
var
  p: Pointer;
begin
  if Pointer(AObj) <> nil then begin
    p := Pointer(AObj);
    Pointer(AObj) := nil;
{$IFDEF AUTOREFCOUNT}
    if TObject(p).__ObjRelease > 0 then
      TObject(p).DisposeOf;
{$ELSE}
    TObject(p).Destroy;
{$ENDIF}
  end;
end;

Pozn: částečně inspirováno některými poznámkami od Dalija Prasnikar


Nabízíme Delphi školení a konzultace na různá témata, primárně ve Vaší firmě.

Tagy: , , , , ,

Jazyk | Novinky v Delphi

Komentáře

12.7.2014 11:41:25 #

tz

Ještě bych doporučil na začátek každého .DPR:

begin
{$IFDEF DEBUG}
  System.ReportMemoryLeaksOnShutdown := True;
{$ENDIF}

  .
  .
  .

end.

tz

13.7.2014 11:22:20 #

frankee

Pokud budou nové kousky přidány tak hloupě, jako například "string anabáze", které v každé delphi verzi používají jiný typ, tak potěš koště...

Pokud to přidají jako "rozšíření" jazyka, nic proti tomu nemám...

frankee

13.7.2014 23:16:02 #

radekc

Frankee - no komentář trochu mimo, ale co už. Jinak jediná změna ohledně řetězců byla v D2009.

Ad rozšíření - evidentně tě moje vysvětlení minulo. Tohle není v žádném případě o rozšíření - to není technicky možné realizovat jako rozšíření.

radekc

15.7.2014 13:01:06 #

oxo

Já největší problém v ARC Delphi vidím v tom, že spousta věcí ze starého kódu (včetně RTL Delphi) přímo závisí na volání destruktoru. Např. pouze TFileStream.Destroy uvolní soubor. Na to ARC Delphi prostě nemyslí. Když už to tedy soudruzi takhle pěkně vymysleli, měli přidat do TFileStream i nějakou proceduru CloseHandle, pomocí které by se handle souboru uvolnila bez volání destruktoru. A když tam CloseHandle není a doopravdy potřebuju uvolnit handle, DisposeOf jedinou správnou volbou. Ale na to prostě stále myslet nemůžu, takže je IMHO lepší psát DisposeOf všude, kde jsem dřív psal Free.

Jinak se vám stane http://qc.embarcadero.com/wc/qcmain.aspx?d=124017 a budete najednou zírat jak tele na nový vrata... A netýká se to jenom streamu...

Už jsme tu problematiku probírali, viz https://plus.google.com/112205186014008010433/posts/L3NPzjg59KF

oxo

15.7.2014 13:04:46 #

radekc

Oxo, ale to je to o čem je celý článek a mimochodem se podívej na to uváděné FDFree

radekc

15.7.2014 13:41:13 #

oxo

Právě, díky, je to tak. Jen bych neřekl, že DisposeOf je háček pro starý kód, ale ve spoustě případů i nutnost v novém kódu, bohužel.

oxo

Komentování ukončeno

Naše nabídka

Partial English version.

MVP
Ing. Radek Červinka - Embarcadero MVP
profil na linkedin, Twitter:@delphicz

Nabízím placené poradenství a konzultace v oblasti programování a vývoje SW.
Dále nabízíme i vývoj speciálního software na zakázku.

Neváhejte nás kontaktovat (i ohledně reklamy nebo burzy práce).

Pokud chcete podpořit tento server libovolnou částkou, můžete použít PayPal. Moc děkuji.

Delphi Certified Developer

O Delphi.cz

Delphi je jediný moderní RAD nástroj podporující tvorbu nativních aplikací pro platformu Win32, Win64 , Mac OSX a na iPhone a Android (s výhledem na další platformy díky FireMonkey) na současném trhu (včetně Windows 8.1).

V současnosti je světová komunita přes dva miliónů vývojářů.

Delphi.cz je nezávislý portál pro uživatele Delphi. Portál není koncipován pro úplné začátečníky, i když i ti se zde nebudou nudit, ale spíše na programátory, kteří již něco znají a chtějí své znalosti dále rozvíjet a sledovat novinky.

Anketa

Poslední komentáře

Comment RSS